From aed26082871dc75513b5521b0fce36444d492e57 Mon Sep 17 00:00:00 2001 From: Kylie Lallak <140771878+Kylie-Lallak@users.noreply.github.com> Date: Tue, 6 May 2025 18:12:02 -0700 Subject: [PATCH 1/5] inital product detail page --- .../ProductPage/ProductPage.module.scss | 144 ++++++++++++-- .../_components/ProductPage/ProductPage.tsx | 69 ++++++- app/(pages)/products/_data/products.ts | 26 ++- package-lock.json | 180 +++++++++--------- package.json | 2 +- 5 files changed, 297 insertions(+), 124 deletions(-) diff --git a/app/(pages)/product/_components/ProductPage/ProductPage.module.scss b/app/(pages)/product/_components/ProductPage/ProductPage.module.scss index b311d3e..24ef481 100644 --- a/app/(pages)/product/_components/ProductPage/ProductPage.module.scss +++ b/app/(pages)/product/_components/ProductPage/ProductPage.module.scss @@ -1,31 +1,139 @@ $mid-gap: var(--spacer); .container { - display: flex; - flex-direction: row; - padding: var(--medium-spacer); - gap: $mid-gap; - background-color: var(--light-foreground); - justify-content: center; + display: flex; + flex-direction: row; + padding: var(--medium-spacer); + gap: $mid-gap; + background-color: var(--light-foreground); + justify-content: center; } .main_img_container { - width: calc(calc(100% - $mid-gap) / 2); - aspect-ratio: calc(5/4); - position: relative; + width: calc(50% - $mid-gap); + aspect-ratio: 5 / 4; + position: relative; + + .main_image { + object-fit: cover; + } +} - .main_image { - object-fit: cover; - } +.name { + font-size: 32px; + color: #3a2779; } .prod_information { - width: calc(calc(100% - $mid-gap) / 2); - display: flex; - flex-direction: column; - gap: var(--spacer); + width: calc(50% - $mid-gap); + display: flex; + flex-direction: column; + gap: var(--spacer); +} + +.description { + color: #7a7a7a; + font-size: 16px; + line-height: 1.5; + max-width: 80%; + margin-bottom: 10px; +} + +.price { + color: #3a2779; + font-size: 32px; +} + +.line { + height: 0.8px; + background-color: #e0e0e0; + width: 80%; + margin: 1em 0; } .add_to_cart_button { - max-width: 400px; -} \ No newline at end of file + max-width: 150px; + padding: 15px; + font-size: 1rem; + background: #3a2779; + color: #fff; + border: none; + border-radius: 4px; + cursor: pointer; + transition: transform 0.2s ease, background 0.2s ease; + + &:active { + transform: scale(0.95); + } + + &:hover { + background-color: #7a39d0; + } + + &.added { + background: #3a2779; + animation: pop 0.3s ease; + } +} + +@keyframes pop { + 0% { + transform: scale(1); + } + 50% { + transform: scale(1.2); + } + 100% { + transform: scale(1); + } +} + +.option_buttons { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; +} + +.option_button { + padding: 0.5rem 1rem; + border: 1.5px solid rgb(188, 188, 188); + color: grey; + background: white; + cursor: pointer; + border-radius: 4px; + transition: all 0.2s ease; + + &:hover { + background: #f7f7f7; + } + + &.selected { + + color: #3a2779; + border: 2px solid #3a2779; + font-weight: 500; + + + } +} + +@media (max-width: 750px) { + .container { + flex-direction: column; + justify-content: center; + align-items: center; + } + + .main_img_container { + width: 80%; + } + + .prod_information { + width: 80%; + } + + + + + +} diff --git a/app/(pages)/product/_components/ProductPage/ProductPage.tsx b/app/(pages)/product/_components/ProductPage/ProductPage.tsx index 254a259..c3dafb8 100644 --- a/app/(pages)/product/_components/ProductPage/ProductPage.tsx +++ b/app/(pages)/product/_components/ProductPage/ProductPage.tsx @@ -1,8 +1,8 @@ 'use client'; +import { useState } from 'react'; import styles from './ProductPage.module.scss'; import Image from 'next/image'; - import { useShoppingCart } from '@hooks/useShoppingCart'; interface Product { @@ -11,23 +11,78 @@ interface Product { alt: string; name: string; price: number; + description: string; + options?: string[]; } -export default function ProductPage({ id, src, alt, name, price }: Product) { +export default function ProductPage({ + id, + src, + alt, + name, + price, + description, + options = [], +}: Product) { const { add_to_cart } = useShoppingCart(); + const [added, setAdded] = useState(false); + const [selectedOption, setSelectedOption] = useState(''); + + const handleAdd = () => { + if (options.length && !selectedOption) { + alert('Please select a size!'); + return; + } + + add_to_cart(id, selectedOption); + setAdded(true); + setTimeout(() => setAdded(false), 1500); + }; + return (
{alt}
+
-

{name}

-

${price}

+

{name}

+

{description}

+
+

${price}

+ + {options.length > 0 && ( +
+ +
+ {options.map((option) => ( + + ))} +
+
+ )} +
diff --git a/app/(pages)/products/_data/products.ts b/app/(pages)/products/_data/products.ts index 39797d6..ad681f1 100644 --- a/app/(pages)/products/_data/products.ts +++ b/app/(pages)/products/_data/products.ts @@ -3,29 +3,39 @@ interface Product { src: string; alt: string; name: string; + description: string; price: number; + options?: string[]; // optional array of selectable options (like sizes or colors) } -const products = [ +const products: Product[] = [ { id: '1', src: '/shop/dummy_data/shib.jpg', alt: 'shib', name: 'shib', + description: + 'About 9 in (23 cm) tall, this Shiba Inu plushie is made from soft, hypoallergenic polyester with embroidered eyes and durable stitching. It’s machine-washable (gentle cycle) and suitable for ages 3 and up.', price: 15, + options: ['Small', 'Medium', 'Large'], }, { id: '2', src: '/shop/dummy_data/shib.jpg', alt: 'shiba number 2', name: 'shiba number 2', + description: + 'About 9 in (23 cm) tall, this Shiba Inu plushie is made from soft, hypoallergenic polyester with embroidered eyes and durable stitching. It’s machine-washable (gentle cycle) and suitable for ages 3 and up.', price: 7, + options: ['Standard'], }, { id: '3', src: '/shop/dummy_data/shib.jpg', alt: 'shiba number 3', name: 'shiba number 3', + description: + 'About 9 in (23 cm) tall, this Shiba Inu plushie is made from soft, hypoallergenic polyester with embroidered eyes and durable stitching. It’s machine-washable (gentle cycle) and suitable for ages 3 and up.', price: 108, }, { @@ -33,22 +43,30 @@ const products = [ src: '/shop/dummy_data/shib.jpg', alt: 'shib', name: 'shiba number 4', + description: + 'About 9 in (23 cm) tall, this Shiba Inu plushie is made from soft, hypoallergenic polyester with embroidered eyes and durable stitching. It’s machine-washable (gentle cycle) and suitable for ages 3 and up.', price: 141.25, + options: ['Standard', 'XL'], }, { id: '5', src: '/shop/dummy_data/shib.jpg', - alt: 'shiba number 2', + alt: 'shiba number 5', name: 'shiba number 5', + description: + 'About 9 in (23 cm) tall, this Shiba Inu plushie is made from soft, hypoallergenic polyester with embroidered eyes and durable stitching. It’s machine-washable (gentle cycle) and suitable for ages 3 and up.', price: 70, }, { id: '6', src: '/shop/dummy_data/shib.jpg', - alt: 'shiba number 3', + alt: 'shiba number 6', name: 'shiba number 6', + description: + 'About 9 in (23 cm) tall, this Shiba Inu plushie is made from soft, hypoallergenic polyester with embroidered eyes and durable stitching. It’s machine-washable (gentle cycle) and suitable for ages 3 and up.', price: 18, + options: ['Small', 'Medium', 'Large'], }, ]; -export default products as Product[]; +export default products; diff --git a/package-lock.json b/package-lock.json index a25bab8..dc221ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "@apollo/client": "^3.9.4", "@apollo/experimental-nextjs-app-support": "^0.8.0", "mongodb": "^6.3.0", - "next": "14.0.4", + "next": "^14.2.28", "react": "^18", "react-dom": "^18", "react-icons": "^4.12.0", @@ -222,9 +222,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.6.tgz", - "integrity": "sha512-zHd0eUrf5GZoOWVCXp6koAKQTfZV07eit6bGPmJgnZdnSAvvZee6zniW2XMF7Cmc4ISOOnPy3QaSiIJGJkVEDQ==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz", + "integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==", "dev": true, "dependencies": { "regenerator-runtime": "^0.14.0" @@ -234,9 +234,9 @@ } }, "node_modules/@babel/runtime-corejs3": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.23.6.tgz", - "integrity": "sha512-Djs/ZTAnpyj0nyg7p1J6oiE/tZ9G2stqAFlLGZynrW+F3k2w2jGK2mLOBxzYIOcZYA89+c3d3wXKpYLcpwcU6w==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.27.0.tgz", + "integrity": "sha512-UWjX6t+v+0ckwZ50Y5ShZLnlk95pP5MyW/pon9tiYzl3+18pkTHTFNTKr7rQbfRXPkowt2QAn30o1b6oswszew==", "dev": true, "dependencies": { "core-js-pure": "^3.30.2", @@ -461,9 +461,9 @@ } }, "node_modules/@next/env": { - "version": "14.0.4", - "resolved": "https://registry.npmjs.org/@next/env/-/env-14.0.4.tgz", - "integrity": "sha512-irQnbMLbUNQpP1wcE5NstJtbuA/69kRfzBrpAD7Gsn8zm/CY6YQYc3HQBz8QPxwISG26tIm5afvvVbu508oBeQ==" + "version": "14.2.28", + "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.28.tgz", + "integrity": "sha512-PAmWhJfJQlP+kxZwCjrVd9QnR5x0R3u0mTXTiZDgSd4h5LdXmjxCCWbN9kq6hkZBOax8Rm3xDW5HagWyJuT37g==" }, "node_modules/@next/eslint-plugin-next": { "version": "14.0.4", @@ -475,9 +475,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "14.0.4", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.0.4.tgz", - "integrity": "sha512-mF05E/5uPthWzyYDyptcwHptucf/jj09i2SXBPwNzbgBNc+XnwzrL0U6BmPjQeOL+FiB+iG1gwBeq7mlDjSRPg==", + "version": "14.2.28", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.28.tgz", + "integrity": "sha512-kzGChl9setxYWpk3H6fTZXXPFFjg7urptLq5o5ZgYezCrqlemKttwMT5iFyx/p1e/JeglTwDFRtb923gTJ3R1w==", "cpu": [ "arm64" ], @@ -490,9 +490,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "14.0.4", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.0.4.tgz", - "integrity": "sha512-IZQ3C7Bx0k2rYtrZZxKKiusMTM9WWcK5ajyhOZkYYTCc8xytmwSzR1skU7qLgVT/EY9xtXDG0WhY6fyujnI3rw==", + "version": "14.2.28", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.28.tgz", + "integrity": "sha512-z6FXYHDJlFOzVEOiiJ/4NG8aLCeayZdcRSMjPDysW297Up6r22xw6Ea9AOwQqbNsth8JNgIK8EkWz2IDwaLQcw==", "cpu": [ "x64" ], @@ -505,9 +505,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "14.0.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.0.4.tgz", - "integrity": "sha512-VwwZKrBQo/MGb1VOrxJ6LrKvbpo7UbROuyMRvQKTFKhNaXjUmKTu7wxVkIuCARAfiI8JpaWAnKR+D6tzpCcM4w==", + "version": "14.2.28", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.28.tgz", + "integrity": "sha512-9ARHLEQXhAilNJ7rgQX8xs9aH3yJSj888ssSjJLeldiZKR4D7N08MfMqljk77fAwZsWwsrp8ohHsMvurvv9liQ==", "cpu": [ "arm64" ], @@ -520,9 +520,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "14.0.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.0.4.tgz", - "integrity": "sha512-8QftwPEW37XxXoAwsn+nXlodKWHfpMaSvt81W43Wh8dv0gkheD+30ezWMcFGHLI71KiWmHK5PSQbTQGUiidvLQ==", + "version": "14.2.28", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.28.tgz", + "integrity": "sha512-p6gvatI1nX41KCizEe6JkF0FS/cEEF0u23vKDpl+WhPe/fCTBeGkEBh7iW2cUM0rvquPVwPWdiUR6Ebr/kQWxQ==", "cpu": [ "arm64" ], @@ -535,9 +535,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "14.0.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.0.4.tgz", - "integrity": "sha512-/s/Pme3VKfZAfISlYVq2hzFS8AcAIOTnoKupc/j4WlvF6GQ0VouS2Q2KEgPuO1eMBwakWPB1aYFIA4VNVh667A==", + "version": "14.2.28", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.28.tgz", + "integrity": "sha512-nsiSnz2wO6GwMAX2o0iucONlVL7dNgKUqt/mDTATGO2NY59EO/ZKnKEr80BJFhuA5UC1KZOMblJHWZoqIJddpA==", "cpu": [ "x64" ], @@ -550,9 +550,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "14.0.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.0.4.tgz", - "integrity": "sha512-m8z/6Fyal4L9Bnlxde5g2Mfa1Z7dasMQyhEhskDATpqr+Y0mjOBZcXQ7G5U+vgL22cI4T7MfvgtrM2jdopqWaw==", + "version": "14.2.28", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.28.tgz", + "integrity": "sha512-+IuGQKoI3abrXFqx7GtlvNOpeExUH1mTIqCrh1LGFf8DnlUcTmOOCApEnPJUSLrSbzOdsF2ho2KhnQoO0I1RDw==", "cpu": [ "x64" ], @@ -565,9 +565,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "14.0.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.0.4.tgz", - "integrity": "sha512-7Wv4PRiWIAWbm5XrGz3D8HUkCVDMMz9igffZG4NB1p4u1KoItwx9qjATHz88kwCEal/HXmbShucaslXCQXUM5w==", + "version": "14.2.28", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.28.tgz", + "integrity": "sha512-l61WZ3nevt4BAnGksUVFKy2uJP5DPz2E0Ma/Oklvo3sGj9sw3q7vBWONFRgz+ICiHpW5mV+mBrkB3XEubMrKaA==", "cpu": [ "arm64" ], @@ -580,9 +580,9 @@ } }, "node_modules/@next/swc-win32-ia32-msvc": { - "version": "14.0.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.0.4.tgz", - "integrity": "sha512-zLeNEAPULsl0phfGb4kdzF/cAVIfaC7hY+kt0/d+y9mzcZHsMS3hAS829WbJ31DkSlVKQeHEjZHIdhN+Pg7Gyg==", + "version": "14.2.28", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.28.tgz", + "integrity": "sha512-+Kcp1T3jHZnJ9v9VTJ/yf1t/xmtFAc/Sge4v7mVc1z+NYfYzisi8kJ9AsY8itbgq+WgEwMtOpiLLJsUy2qnXZw==", "cpu": [ "ia32" ], @@ -595,9 +595,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "14.0.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.0.4.tgz", - "integrity": "sha512-yEh2+R8qDlDCjxVpzOTEpBLQTEFAcP2A8fUFLaWNap9GitYKkKv1//y2S6XY6zsR4rCOPRpU7plYDR+az2n30A==", + "version": "14.2.28", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.28.tgz", + "integrity": "sha512-1gCmpvyhz7DkB1srRItJTnmR2UwQPAUXXIg9r0/56g3O8etGmwlX68skKXJOp9EejW3hhv7nSQUJ2raFiz4MoA==", "cpu": [ "x64" ], @@ -680,11 +680,17 @@ "integrity": "sha512-UY+FGM/2jjMkzQLn8pxcHGMaVLh9aEitG3zY2CiY7XHdLiz3bZOwa6oDxNqEMv7zZkV+cj5DOdz0cQ1BP5Hjgw==", "dev": true }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==" + }, "node_modules/@swc/helpers": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.2.tgz", - "integrity": "sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==", + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz", + "integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==", "dependencies": { + "@swc/counter": "^0.1.3", "tslib": "^2.4.0" } }, @@ -1415,11 +1421,11 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -1536,9 +1542,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001570", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001570.tgz", - "integrity": "sha512-+3e0ASu4sw1SWaoCtvPeyXp+5PsjigkSt8OXZbF9StH5pQWbxEjLAZE3n8Aup5udop1uRiKA7a4utUk/uoSpUw==", + "version": "1.0.30001715", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001715.tgz", + "integrity": "sha512-7ptkFGMm2OAOgvZpwgA4yjQ5SQbrNVGdRjzH0pBdy1Fasvcr+KAeECmbCAECzTuDuoX0FCY8KzUxjf9+9kfZEw==", "funding": [ { "type": "opencollective", @@ -1729,9 +1735,9 @@ } }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "dependencies": { "path-key": "^3.1.0", @@ -2995,9 +3001,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -3239,11 +3245,6 @@ "node": ">=10.13.0" } }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" - }, "node_modules/globals": { "version": "13.24.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", @@ -4224,12 +4225,12 @@ } }, "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { @@ -4359,9 +4360,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "funding": [ { "type": "github", @@ -4382,18 +4383,17 @@ "dev": true }, "node_modules/next": { - "version": "14.0.4", - "resolved": "https://registry.npmjs.org/next/-/next-14.0.4.tgz", - "integrity": "sha512-qbwypnM7327SadwFtxXnQdGiKpkuhaRLE2uq62/nRul9cj9KhQ5LhHmlziTNqUidZotw/Q1I9OjirBROdUJNgA==", + "version": "14.2.28", + "resolved": "https://registry.npmjs.org/next/-/next-14.2.28.tgz", + "integrity": "sha512-QLEIP/kYXynIxtcKB6vNjtWLVs3Y4Sb+EClTC/CSVzdLD1gIuItccpu/n1lhmduffI32iPGEK2cLLxxt28qgYA==", "dependencies": { - "@next/env": "14.0.4", - "@swc/helpers": "0.5.2", + "@next/env": "14.2.28", + "@swc/helpers": "0.5.5", "busboy": "1.6.0", - "caniuse-lite": "^1.0.30001406", + "caniuse-lite": "^1.0.30001579", "graceful-fs": "^4.2.11", "postcss": "8.4.31", - "styled-jsx": "5.1.1", - "watchpack": "2.4.0" + "styled-jsx": "5.1.1" }, "bin": { "next": "dist/bin/next" @@ -4402,18 +4402,19 @@ "node": ">=18.17.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "14.0.4", - "@next/swc-darwin-x64": "14.0.4", - "@next/swc-linux-arm64-gnu": "14.0.4", - "@next/swc-linux-arm64-musl": "14.0.4", - "@next/swc-linux-x64-gnu": "14.0.4", - "@next/swc-linux-x64-musl": "14.0.4", - "@next/swc-win32-arm64-msvc": "14.0.4", - "@next/swc-win32-ia32-msvc": "14.0.4", - "@next/swc-win32-x64-msvc": "14.0.4" + "@next/swc-darwin-arm64": "14.2.28", + "@next/swc-darwin-x64": "14.2.28", + "@next/swc-linux-arm64-gnu": "14.2.28", + "@next/swc-linux-arm64-musl": "14.2.28", + "@next/swc-linux-x64-gnu": "14.2.28", + "@next/swc-linux-x64-musl": "14.2.28", + "@next/swc-win32-arm64-msvc": "14.2.28", + "@next/swc-win32-ia32-msvc": "14.2.28", + "@next/swc-win32-x64-msvc": "14.2.28" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.41.2", "react": "^18.2.0", "react-dom": "^18.2.0", "sass": "^1.3.0" @@ -4422,6 +4423,9 @@ "@opentelemetry/api": { "optional": true }, + "@playwright/test": { + "optional": true + }, "sass": { "optional": true } @@ -6564,18 +6568,6 @@ "spdx-expression-parse": "^3.0.0" } }, - "node_modules/watchpack": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", - "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", - "dependencies": { - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.1.2" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", diff --git a/package.json b/package.json index fbb2e3f..935a10a 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "@apollo/client": "^3.9.4", "@apollo/experimental-nextjs-app-support": "^0.8.0", "mongodb": "^6.3.0", - "next": "14.0.4", + "next": "^14.2.28", "react": "^18", "react-dom": "^18", "react-icons": "^4.12.0", From b87cfb3af611ed2dc649b18704d99daaa0a6b7cd Mon Sep 17 00:00:00 2001 From: Kylie Lallak <140771878+Kylie-Lallak@users.noreply.github.com> Date: Thu, 29 May 2025 01:31:25 -0700 Subject: [PATCH 2/5] added discounts, and multiple customizations, maybe fixed shopping cart?? --- app/(pages)/_contexts/ShoppingCartContext.tsx | 102 ++++++++++++------ .../ProductPage/ProductPage.module.scss | 28 ++++- .../_components/ProductPage/ProductPage.tsx | 55 +++++++--- app/(pages)/products/_data/products.ts | 60 +++++++---- 4 files changed, 170 insertions(+), 75 deletions(-) diff --git a/app/(pages)/_contexts/ShoppingCartContext.tsx b/app/(pages)/_contexts/ShoppingCartContext.tsx index b276e20..b96286f 100644 --- a/app/(pages)/_contexts/ShoppingCartContext.tsx +++ b/app/(pages)/_contexts/ShoppingCartContext.tsx @@ -1,4 +1,5 @@ 'use client'; + import { createContext, useState, useEffect, useCallback } from 'react'; import useLocalStorage from '@hooks/useLocalStorage'; import products from '../products/_data/products'; @@ -8,9 +9,7 @@ export const ShoppingCartContext = createContext({ cart: [], add_to_cart: (_: string) => {}, remove_from_cart: (_: string) => {}, - compute_total: () => { - return 0; - }, + compute_total: () => 0, }); interface CartItem { @@ -19,26 +18,30 @@ interface CartItem { prod_name: string; prod_price: number; quantity: number; + selectedOptions?: Record; + discountPercent?: number; } interface LocalItem { prod_id: string; quantity: number; + selectedOptions?: Record; + discountPercent?: number; } interface ShoppingCartContextInt { loading: boolean; cart: CartItem[]; - add_to_cart: (id: string) => void; + add_to_cart: ( + id: string, + selectedOptions?: Record, + discountPercent?: number + ) => void; remove_from_cart: (id: string) => void; compute_total: () => number; } -export function ShoppingCartProvider({ - children, -}: { - children: React.ReactNode; -}) { +export function ShoppingCartProvider({ children }: { children: React.ReactNode }) { const [loading, setLoading] = useState(true); const [cart, setCart] = useState([]); const { @@ -50,44 +53,75 @@ export function ShoppingCartProvider({ useEffect(() => { if (!cartLoading) { try { - const local_data = JSON.parse(data); - const filtered_local_data = local_data.filter( - (local_item: LocalItem) => - products.filter((p) => p.id === local_item.prod_id)[0] - ? true - : false + const local_data: LocalItem[] = + typeof data === 'string' ? JSON.parse(data) : data; + + const filtered_local_data = local_data.filter((local_item) => + products.some((p) => p.id === local_item.prod_id) ); if (filtered_local_data.length !== local_data.length) { updateValue(JSON.stringify(filtered_local_data)); - } else { - const expanded_data = local_data.map((local_item: LocalItem) => { - const item = products.filter((p) => p.id === local_item.prod_id)[0]; + } + + const expanded_data = filtered_local_data + .map((local_item: LocalItem) => { + const item = products.find((p) => p.id === local_item.prod_id); + if (!item) return null; + + const discount = local_item.discountPercent ?? item.discountPercent; + const price = discount + ? Number((item.price * (1 - discount / 100)).toFixed(2)) + : item.price; + return { prod_id: item.id, img_url: item.src, prod_name: item.name, - prod_price: item.price, + prod_price: price, quantity: local_item.quantity, + selectedOptions: local_item.selectedOptions, + discountPercent: discount, }; - }); - setCart(expanded_data); - } + }) + .filter((item) => item !== null) as CartItem[]; + + setCart(expanded_data); } catch (e) { updateValue(JSON.stringify([])); setCart([]); } + setLoading(false); } }, [cartLoading, data, updateValue]); const add_to_cart = useCallback( - (id: string) => { - const cart_data = JSON.parse(data); - cart_data.push({ - prod_id: id, - quantity: 1, - }); + ( + id: string, + selectedOptions: Record = {}, + discountPercent?: number + ) => { + const cart_data: LocalItem[] = + typeof data === 'string' ? JSON.parse(data) : data; + + const existingIndex = cart_data.findIndex( + (item) => + item.prod_id === id && + JSON.stringify(item.selectedOptions) === JSON.stringify(selectedOptions) + ); + + if (existingIndex !== -1) { + cart_data[existingIndex].quantity += 1; + } else { + cart_data.push({ + prod_id: id, + quantity: 1, + selectedOptions, + discountPercent, + }); + } + updateValue(JSON.stringify(cart_data)); }, [data, updateValue] @@ -95,19 +129,17 @@ export function ShoppingCartProvider({ const remove_from_cart = useCallback( (id: string) => { - const cart_data = JSON.parse(data); - const new_cart = cart_data.filter( - (item: LocalItem) => item.prod_id !== id - ); + const cart_data: LocalItem[] = + typeof data === 'string' ? JSON.parse(data) : data; + + const new_cart = cart_data.filter((item) => item.prod_id !== id); updateValue(JSON.stringify(new_cart)); }, [data, updateValue] ); const compute_total = useCallback(() => { - let sum = 0; - cart.forEach((item: CartItem) => (sum += item.prod_price * item.quantity)); - return sum; + return cart.reduce((sum, item) => sum + item.prod_price * item.quantity, 0); }, [cart]); const contextValue = { diff --git a/app/(pages)/product/_components/ProductPage/ProductPage.module.scss b/app/(pages)/product/_components/ProductPage/ProductPage.module.scss index 24ef481..92af27b 100644 --- a/app/(pages)/product/_components/ProductPage/ProductPage.module.scss +++ b/app/(pages)/product/_components/ProductPage/ProductPage.module.scss @@ -14,6 +14,7 @@ $mid-gap: var(--spacer); aspect-ratio: 5 / 4; position: relative; + .main_image { object-fit: cover; } @@ -39,8 +40,23 @@ $mid-gap: var(--spacer); margin-bottom: 10px; } + .price { + font-size: 32px; + display: flex; + gap: 0.5rem; + align-items: center; +} + +.original_price { + color: #999; + text-decoration: line-through; + font-size: 16px; +} + +.discounted_price { color: #3a2779; + font-weight: 600; font-size: 32px; } @@ -116,6 +132,13 @@ $mid-gap: var(--spacer); } } +.customization_label { + font-weight: 400; + margin-bottom: 0.5rem; + color: grey; + + +} @media (max-width: 750px) { .container { @@ -132,8 +155,7 @@ $mid-gap: var(--spacer); width: 80%; } - - - } + + diff --git a/app/(pages)/product/_components/ProductPage/ProductPage.tsx b/app/(pages)/product/_components/ProductPage/ProductPage.tsx index c3dafb8..dc3e4ea 100644 --- a/app/(pages)/product/_components/ProductPage/ProductPage.tsx +++ b/app/(pages)/product/_components/ProductPage/ProductPage.tsx @@ -12,9 +12,11 @@ interface Product { name: string; price: number; description: string; - options?: string[]; + customizations?: Record; + discountPercent?: number; // add discount percent as whole int, automatocly applies it to price } + export default function ProductPage({ id, src, @@ -22,19 +24,28 @@ export default function ProductPage({ name, price, description, - options = [], -}: Product) { + customizations = {}, + discountPercent, // ✅ add this line +}: Product) +{ const { add_to_cart } = useShoppingCart(); const [added, setAdded] = useState(false); - const [selectedOption, setSelectedOption] = useState(''); + const [selectedOptions, setSelectedOptions] = useState>({}); + const discountedPrice = discountPercent + ? (price * (1 - discountPercent / 100)).toFixed(2) + : null; const handleAdd = () => { - if (options.length && !selectedOption) { - alert('Please select a size!'); + // Ensure every customization has a selected option + const missing = Object.keys(customizations).find( + (key) => !selectedOptions[key] + ); + if (missing) { + alert(`Please select a ${missing}!`); return; } - add_to_cart(id, selectedOption); + add_to_cart(id, selectedOptions); setAdded(true); setTimeout(() => setAdded(false), 1500); }; @@ -49,29 +60,41 @@ export default function ProductPage({

{name}

{description}

-

${price}

+

+ {discountedPrice ? ( + <> + ${price.toFixed(2)} + ${discountedPrice} + + ) : ( + <>${price.toFixed(2)} + )} +

- {options.length > 0 && ( -
- + {Object.entries(customizations).map(([category, options]) => ( +
+

{category}

{options.map((option) => ( ))}
- )} + ))}