diff --git a/06_js-debug/uebungen/u17-19_shop-vitest/.eslintrc.cjs b/06_js-debug/uebungen/u17-19_shop-vitest/.eslintrc.cjs new file mode 100644 index 0000000..d6c9537 --- /dev/null +++ b/06_js-debug/uebungen/u17-19_shop-vitest/.eslintrc.cjs @@ -0,0 +1,18 @@ +module.exports = { + root: true, + env: { browser: true, es2020: true }, + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:react-hooks/recommended', + ], + ignorePatterns: ['dist', '.eslintrc.cjs'], + parser: '@typescript-eslint/parser', + plugins: ['react-refresh'], + rules: { + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, +} diff --git a/06_js-debug/uebungen/u17-19_shop-vitest/README.md b/06_js-debug/uebungen/u17-19_shop-vitest/README.md new file mode 100644 index 0000000..7d128c6 --- /dev/null +++ b/06_js-debug/uebungen/u17-19_shop-vitest/README.md @@ -0,0 +1,10 @@ +# Frontend Class: VogueJunction Starter Template + +Install the dependencies and devDependencies and start the server. + +```bash +npm install +npm run dev +``` + +This will start the server on http://localhost:5173 diff --git a/06_js-debug/uebungen/u17-19_shop-vitest/index.html b/06_js-debug/uebungen/u17-19_shop-vitest/index.html new file mode 100644 index 0000000..6574d3c --- /dev/null +++ b/06_js-debug/uebungen/u17-19_shop-vitest/index.html @@ -0,0 +1,12 @@ + + + + + + Vogue Junction + + +
+ + + diff --git a/06_js-debug/uebungen/u17-19_shop-vitest/package.json b/06_js-debug/uebungen/u17-19_shop-vitest/package.json new file mode 100644 index 0000000..923ef63 --- /dev/null +++ b/06_js-debug/uebungen/u17-19_shop-vitest/package.json @@ -0,0 +1,38 @@ +{ + "name": "01_shop-vitest", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview", + "test": "vitest", + "test:ui": "npx vitest --ui" + }, + "dependencies": { + "react": "^19.2.8", + "react-dom": "^19.2.8", + "react-router-dom": "^7.18.2" + }, + "devDependencies": { + "@testing-library/dom": "^10.4.1", + "@testing-library/jest-dom": "^7.0.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.5", + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.4", + "@typescript-eslint/eslint-plugin": "^8.67.0", + "@typescript-eslint/parser": "^8.67.0", + "@vitejs/plugin-react": "^6.1.0", + "@vitest/ui": "^4.1.11", + "eslint": "^10.8.1", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.4", + "jsdom": "^30.0.1", + "typescript": "^6.0.3", + "vite": "^8.2.2", + "vitest": "^4.1.11" + } +} diff --git a/06_js-debug/uebungen/u17-19_shop-vitest/public/vite.svg b/06_js-debug/uebungen/u17-19_shop-vitest/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/06_js-debug/uebungen/u17-19_shop-vitest/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/06_js-debug/uebungen/u17-19_shop-vitest/src/components/CategorySection.tsx b/06_js-debug/uebungen/u17-19_shop-vitest/src/components/CategorySection.tsx new file mode 100644 index 0000000..4fb8862 --- /dev/null +++ b/06_js-debug/uebungen/u17-19_shop-vitest/src/components/CategorySection.tsx @@ -0,0 +1,77 @@ +import React from 'react'; + +type CategorySectionProps = { + categories: { + imgSrc: string; + imgAlt: string; + href: string; + linkText: string; + }[]; +}; + +import { Link } from 'react-router-dom'; + +const CategorySection: React.FC = ({ categories }) => { + return ( +
+
+

+ Explore by Category +

+ +
+ + {categories[0].imgAlt} + + +
+ ); +}; + +export default CategorySection; diff --git a/06_js-debug/uebungen/u17-19_shop-vitest/src/components/Footer.tsx b/06_js-debug/uebungen/u17-19_shop-vitest/src/components/Footer.tsx new file mode 100644 index 0000000..86ea22e --- /dev/null +++ b/06_js-debug/uebungen/u17-19_shop-vitest/src/components/Footer.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; + +type NavigationItem = { + name: string; + href: string; +}; + +const navigationItems: NavigationItem[] = [ + { + name: 'Home', + href: '/', + }, + { + name: 'Women', + href: '/women', + }, + { + name: 'Men', + href: '/men', + }, + { + name: 'Accessories', + href: '/accessories', + }, +]; + +type FooterProps = { + navigation?: NavigationItem[]; +}; + +const Footer: React.FC = ({ navigation = navigationItems }) => { + return ( +
+
+
+ {navigation.map((item) => ( + + {item.name} + + ))} +
+ +
+

+ © {new Date().getFullYear()} Vogue Junction, Inc. All rights + reserved. +

+
+
+
+ ); +}; + +export default Footer; diff --git a/06_js-debug/uebungen/u17-19_shop-vitest/src/components/Header.tsx b/06_js-debug/uebungen/u17-19_shop-vitest/src/components/Header.tsx new file mode 100644 index 0000000..ae21fd5 --- /dev/null +++ b/06_js-debug/uebungen/u17-19_shop-vitest/src/components/Header.tsx @@ -0,0 +1,53 @@ +import { Link } from 'react-router-dom'; + +const Header = () => { + return ( +
+ {/* Mobile Header */} +
+
+ +
Vogue Junction
+ +
+ +
+ + Women + + + Men + + + Accessories + +
+
+ + {/* Desktop Header */} +
+
+ +
Vogue Junction
+ + +
+ + Women + + + Men + + + Accessories + +
+
+
+ +
+
+ ); +}; + +export default Header; diff --git a/06_js-debug/uebungen/u17-19_shop-vitest/src/components/Hero.tsx b/06_js-debug/uebungen/u17-19_shop-vitest/src/components/Hero.tsx new file mode 100644 index 0000000..afa8772 --- /dev/null +++ b/06_js-debug/uebungen/u17-19_shop-vitest/src/components/Hero.tsx @@ -0,0 +1,23 @@ +import React from 'react'; + +type HeroProps = { + companyName?: string; +}; + +const Hero: React.FC = ({ companyName = 'Vogue Junction' }) => { + return ( +
+
+

Welcome to the world of {companyName}!

+

+ Discover a diverse collection of stylish products for everyone. From + trendy apparel and accessories to timeless classics, we offer + something for every taste and occasion. Shop now and elevate your + style! +

+
+
+ ); +}; + +export default Hero; diff --git a/06_js-debug/uebungen/u17-19_shop-vitest/src/components/ImageGallery.tsx b/06_js-debug/uebungen/u17-19_shop-vitest/src/components/ImageGallery.tsx new file mode 100644 index 0000000..b68a3af --- /dev/null +++ b/06_js-debug/uebungen/u17-19_shop-vitest/src/components/ImageGallery.tsx @@ -0,0 +1,71 @@ +import React, { useState, useEffect } from 'react'; +import { Product } from '../lib/datatypes'; + +type ImageGalleryProps = { + product: Product; +}; + +const ImageGallery: React.FC = ({ product }: { product: Product }) => { + const [selectedImage, setSelectedImage] = useState(product.images[0]); + const [selectedIndex, setSelectedIndex] = useState(0); + + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === 'ArrowRight') { + setSelectedIndex((prevIndex) => { + const nextIndex = (prevIndex + 1) % product.images.length; + setSelectedImage(product.images[nextIndex]); + return nextIndex; + }); + } + + if (event.key === 'ArrowLeft') { + setSelectedIndex((prevIndex) => { + const nextIndex = (prevIndex - 1 + product.images.length) % product.images.length; + setSelectedImage(product.images[nextIndex]); + return nextIndex; + }); + } + }; + + useEffect(() => { + window.addEventListener('keydown', handleKeyDown); + + return () => { + window.removeEventListener('keydown', handleKeyDown); + }; + }, [product.images]); + + return ( +
+ {/* Image selector */} +
+
+ {product.images.map((image, index) => ( + + ))} +
+
+ + {/* Main Image Display */} +
+ {product.title} +
+
+ ); +}; + +export default ImageGallery; diff --git a/06_js-debug/uebungen/u17-19_shop-vitest/src/components/ProductList.tsx b/06_js-debug/uebungen/u17-19_shop-vitest/src/components/ProductList.tsx new file mode 100644 index 0000000..17f7234 --- /dev/null +++ b/06_js-debug/uebungen/u17-19_shop-vitest/src/components/ProductList.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { Product } from '../lib/datatypes'; + +import ProductListCard from './ProductListCard'; + +type ProductListProps = { + data: Product[]; +}; + +const ProductList: React.FC = ({ data }) => { + return ( +
+

Products

+ +
+ {data.map((product) => ( + + ))} +
+
+ ); +}; + +export default ProductList; diff --git a/06_js-debug/uebungen/u17-19_shop-vitest/src/components/ProductListCard.tsx b/06_js-debug/uebungen/u17-19_shop-vitest/src/components/ProductListCard.tsx new file mode 100644 index 0000000..dcf4832 --- /dev/null +++ b/06_js-debug/uebungen/u17-19_shop-vitest/src/components/ProductListCard.tsx @@ -0,0 +1,65 @@ +import React from 'react'; +import { Product } from '../lib/datatypes'; + +import { Link } from 'react-router-dom'; + +const ProductListCard = ({ product }: { product: Product }) => { + const { id, title, price, rating, stock, thumbnail } = product; + + return ( +
+
+ {title} +
+ +
+

+ +

+ +
+

{Math.round(rating)} out of 5 stars

+ +
+ {[0, 1, 2, 3, 4].map((ratingNumber) => ( + ratingNumber ? '#1e293b' : '#e5e7eb', + }} + aria-hidden="true" + /> + ))} +
+ +

{stock} in stock

+
+ +

+ {Number(price).toLocaleString('en-US', { + style: 'currency', + currency: 'USD', + })} +

+
+
+ ); +}; + +export default ProductListCard; + +function StarIcon(props: React.SVGProps) { + return ( + + + + ); +} diff --git a/06_js-debug/uebungen/u17-19_shop-vitest/src/index.css b/06_js-debug/uebungen/u17-19_shop-vitest/src/index.css new file mode 100644 index 0000000..a483ad3 --- /dev/null +++ b/06_js-debug/uebungen/u17-19_shop-vitest/src/index.css @@ -0,0 +1,798 @@ +*, +*::before, +*::after { + box-sizing: border-box; +} + +*, +html, +body { + margin: 0; + padding: 0; + font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, + Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; + color: #0f172a; +} + +button { + background-color: transparent; + border: none; +} + +a { + text-decoration: none; + color: inherit; +} + +header { + position: relative; + width: 100%; +} + +footer { + border: 1px solid #e5e7eb; + border-top-width: 1px; + margin-top: 3.5rem /* 56px */; +} + +.screen-reader-text { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; +} + +.star-icons { + height: 1.25rem /* 20px */; + width: 1.25rem /* 20px */; + flex-shrink: 0; +} + +/* ===== Header Component ===== */ +.logo-text { + font-size: 1.25rem /* 20px */; + line-height: 1.75rem /* 28px */; + font-weight: 600; + color: #1f2937; +} + +.header-container { + position: relative; + display: flex; + align-items: center; + justify-content: space-between; + padding: 1.5rem 1rem; /* 24px 16px */ + max-width: 80rem; /* 1280px */ + margin: 0 auto; +} + +.mobile-header-wrapper { + display: block; +} + +.mobile-header-links-container { + display: flex; + justify-content: center; + gap: 1.5rem; /* 24px */ + padding-bottom: 1rem /* 16px */; +} + +.mobile-header-logo-link { + display: flex; + align-items: center; + justify-content: center; + gap: 1rem; /* 16px */ + width: 100%; +} + +.desktop-header-wrapper { + display: none; +} + +.desktop-header-logo-link { + display: flex; + align-items: center; + gap: 1rem; /* 16px */ +} + +.desktop-header-links-container { + position: absolute; + left: 50%; + transform: translateX(-50%); + display: flex; + gap: 1.5rem; /* 24px */ +} + +.header-link-text { + padding: 0.5rem /* 8px */; + color: #1f2937; +} + +.header-link-text:hover { + color: #4b5563; +} + +.header-border { + border-bottom-width: 2px; + width: 100%; + box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); + border-color: #f3f4f6; +} + +/* ===== Hero Component ===== */ +.hero-wrapper { + max-width: 42rem /* 672px */; + margin: 0 auto; + padding: 6rem 1rem; /* 96px 16px */ +} + +.hero-text-container { + text-align: center; +} + +.hero-title { + font-size: 2.25rem /* 36px */; + line-height: 2.5rem /* 40px */; + font-weight: 700; + letter-spacing: -0.025em; + color: #111827; +} + +.hero-subtitle { + margin-top: 2.5rem /* 40px */; + font-size: 1.125rem /* 18px */; + line-height: 2rem /* 32px */; + color: #4b5563; +} + +/* ===== CategorySection Component ===== */ +.category-section-wrapper { + margin: 0 auto; + max-width: 80rem /* 1280px */; + padding: 2.5rem 1rem; /* 40px 16px */ +} + +.category-section-title { + font-size: 1.5rem /* 24px */; + line-height: 2rem /* 32px */; + font-weight: 700; + letter-spacing: -0.025em; + color: #111827; +} + +.category-section-grid-wrapper { + margin-top: 1.5rem /* 24px */; + display: grid; + grid-template-columns: repeat(1, minmax(0, 1fr)); + row-gap: 1.5rem /* 24px */; +} + +.category-section-grid-main { + position: relative; + overflow: hidden; + border-radius: 0.5rem /* 8px */; + aspect-ratio: 1 / 2; + width: 100%; + height: 100%; + max-height: 250px; +} + +.category-section-grid-main:hover { + opacity: 0.85; +} + +.category-section-grid-main-img { + height: 100%; + width: 100%; + object-fit: cover; + object-position: top; + filter: grayscale(100%); +} + +.category-section-gradient-bg { + position: absolute; + bottom: 0; + z-index: 100; + height: 100%; + width: 100%; + background: linear-gradient(to bottom, transparent, black); + opacity: 0.5; +} + +.category-section-cta-container { + position: absolute; + z-index: 100; + bottom: 0; + display: flex; + align-items: flex-end; + padding: 1.5rem /* 24px */; +} + +.category-section-cta-title { + font-weight: 600; + color: white; +} + +.category-section-cta-span { + position: absolute; + inset: 0px; + z-index: 100; +} + +.category-section-cta { + margin-top: 0.25rem /* 4px */; + font-size: 0.875rem /* 14px */; + line-height: 1.25rem /* 20px */; + color: white; +} + +.category-section-categories-container { + position: relative; + aspect-ratio: 1 / 2; + overflow: hidden; + border-radius: 0.5rem /* 8px */; + height: 100%; + width: 100%; + max-height: 250px; +} + +.category-section-categories-container:hover { + opacity: 0.85; +} + +.category-section-categories-img { + height: 100%; + width: 100%; + object-fit: cover; + object-position: center; + filter: grayscale(100%); +} + +.category-section-categories-cta-container { + position: absolute; + z-index: 100; + bottom: 0; + display: flex; + align-items: flex-end; + padding: 1.5rem; /* 24px */ +} + +/* ===== Footer Component ===== */ +.footer-wrapper { + margin: 0 auto; + max-width: 80rem /* 1280px */; + padding: 3rem 1.5rem; /* 48px 24px */ +} + +.footer-links-container { + display: flex; + justify-content: center; + gap: 1.5rem; /* 24px */ +} + +.footer-links-text { + color: #9ca3af; +} + +.footer-links-text:hover { + color: #4b5563; +} + +.footer-copyright { + margin-top: 2rem /* 32px */; +} + +.footer-copyright-text { + text-align: center; + font-size: 0.75rem /* 12px */; + line-height: 1rem /* 16px */; + line-height: 1.25rem /* 20px */; + color: #6b7280; +} + +/* ===== ProductList Component ===== */ +.products-list { + margin: 0 -1px; + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +/* ===== ProductListCard Component ===== */ +.product-card-container { + position: relative; + padding: 1rem; +} + +.product-card-img-wrapper { + aspect-ratio: 1 / 1; + overflow: hidden; + border-radius: 0.5rem /* 8px */; + background-color: white; +} + +.product-card-container:hover .product-card-img-wrapper { + opacity: 0.75; +} + +.product-card-img { + height: 100%; + width: 100%; + object-fit: cover; + object-position: center; +} + +.product-card-info-box { + padding-bottom: 1rem /* 16px */; + padding-top: 2.5rem /* 40px */; + text-align: center; +} + +.product-card-name { + font-size: 0.875rem /* 14px */; + line-height: 1.25rem /* 20px */; + font-weight: 500; + color: #111827; +} + +.product-card-name-span { + position: absolute; + inset: 0px; +} + +.product-card-desc-box { + margin-top: 0.75rem /* 12px */; + display: flex; + flex-direction: column; + align-items: center; +} + +.product-card-ratings-wrapper { + display: flex; + align-items: center; +} + +.product-card-stock { + margin-top: 0.25rem /* 4px */; + font-size: 0.875rem /* 14px */; + line-height: 1.25rem /* 20px */; + color: #6b7280; +} + +.product-card-price { + margin-top: 1rem /* 16px */; + font-size: 1rem /* 16px */; + line-height: 1.5rem /* 24px */; + font-weight: 500; + color: #111827; +} + +/* ===== ImageGallery Component ===== */ +.image-gallery-wrapper { + display: flex; + flex-direction: column-reverse; +} + +.image-selector-wrapper { + margin-left: auto; + margin-right: auto; + margin-top: 1.5rem /* 24px */; + display: none; + width: 100%; + max-width: 42rem /* 672px */; +} + +.image-selector-container { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 1.5rem /* 24px */; +} + +.image-selector-btn { + position: relative; + display: flex; + height: 6rem /* 96px */; + cursor: pointer; + align-items: center; + justify-content: center; + border-radius: 0.375rem /* 6px */; + background-color: white; + font-size: 0.875rem /* 14px */; + line-height: 1.25rem /* 20px */; + font-weight: 500; + text-transform: uppercase; + color: #111827; +} + +.image-selector-btn:hover { + background-color: #f9fafb; +} + +.image-selector-img-wrapper { + position: absolute; + inset: 0px; + display: flex; + overflow: hidden; + border-radius: 0.375rem /* 6px */; +} + +.image-selector-img { + height: 100%; + width: 100%; + object-fit: cover; + object-position: center; +} + +.image-selector-main-img-wrapper { + aspect-ratio: 1 / 1; + width: 100%; +} + +.image-selector-main-img { + height: 100%; + width: 100%; + object-fit: cover; + object-position: center; +} + +/* ===== Category Page ===== */ +.category-page-wrapper { + padding: 3.5rem 0; /* 56px 0 */ +} + +.category-page-container { + margin: 0 auto; + max-width: 80rem /* 1280px */; + overflow: hidden; + padding: 0 1rem; /* 0 16px */ +} + +.category-page-header { + display: flex; + justify-content: space-between; + align-items: top; +} + +.category-page-header-title { + font-size: 1.5rem /* 24px */; + line-height: 2rem /* 32px */; + font-weight: 700; + letter-spacing: -0.025em; + color: #111827; + margin-bottom: 2rem /* 32px */; +} + +.category-page-select { + margin: 0.5rem 0; /* 8px 0 */ + display: block; + width: 11rem /* 176px */; + border: 1px solid #d1d5db; + border-radius: 0.375rem /* 6px */; + padding-top: 0.5rem /* 8px */; + padding-bottom: 0.5rem /* 8px */; + color: #0f172a; +} + +.category-page-select:focus { + border: 1px solid #1e293b; +} + +.category-page-products-wrapper { + margin-top: 2.5rem /* 40px */; +} + +/* ===== Single Product Page ===== */ +.back-button-wrapper { + margin: 0 auto; + max-width: 42rem /* 672px */; + padding-left: 1rem /* 16px */; + padding-right: 1rem /* 16px */; + padding-top: 2rem /* 32px */; +} + +.back-button { + display: flex; + align-items: center; + margin-bottom: 1rem /* 16px */; + font-weight: 500; + margin-top: 1.5rem /* 24px */; +} + +.back-button-icon { + height: 1.5rem /* 24px */; + width: 1.5rem /* 24px */; + stroke: #1e293b; +} + +.back-button-icon-span { + margin-left: 0.75rem /* 12px */; + font-size: 1rem /* 16px */; + line-height: 1.5rem /* 24px */; +} + +.product-page-wrapper { + margin: 0 auto; + max-width: 42rem /* 672px */; + padding: 2.5rem 1rem; /* 40px 16px */ +} + +.product-page-container { + display: block; +} + +.product-page-info-container { + margin-top: 2.5rem /* 40px */; + padding-left: 1rem /* 16px */; + padding-right: 1rem /* 16px */; +} + +.product-page-info-title { + font-size: 1.875rem /* 30px */; + line-height: 2.25rem /* 36px */; + font-weight: 700; + letter-spacing: -0.025em; + color: #111827; +} + +.product-page-mt-3 { + margin-top: 0.75rem /* 12px */; +} + +.product-page-mt-6 { + margin-top: 1.5rem /* 24px */; +} + +.product-page-text { + font-size: 0.875rem /* 14px */; + line-height: 1.25rem /* 20px */; + color: #6b7280; +} + +.product-page-info-price { + font-size: 1.875rem /* 30px */; + line-height: 2.25rem /* 36px */; + letter-spacing: -0.025em; + color: #111827; +} + +.product-page-info-rating-container { + display: flex; + align-items: center; +} + +.product-page-description { + font-size: 1rem /* 16px */; + line-height: 1.5rem /* 24px */; + line-height: 2; + color: #374151; +} + +.product-page-tags-container { + display: flex; + align-items: center; + gap: 0.5rem; /* 8px */ +} + +.product-page-tag { + font-size: 0.75rem /* 12px */; + line-height: 1rem /* 16px */; + font-weight: 500; + text-transform: uppercase; + padding: 0.25rem 0.5rem; + border-radius: 9999px; + background-color: #1e293b; + color: white; +} + +.product-page-reviews-wrapper { + margin-top: 3.5rem /* 56px */; +} + +.product-page-reviews-title { + font-size: 1.5rem /* 24px */; + line-height: 2rem /* 32px */; + font-weight: 500; + color: #111827; +} + +.product-page-reviews-container { + display: flex; + flex-direction: column; + gap: 1.25rem /* 20px */; + margin-top: 0.5rem /* 8px */; +} + +.product-page-ratings-box { + display: flex; + align-items: center; +} + +.product-page-reviews-text-box { + display: flex; + align-items: center; + gap: 1rem; /* 16px */ + margin-top: 0.5rem /* 8px */; +} + +.product-page-reviews-comment { + margin-top: 0.5rem /* 8px */; + font-size: 1.125rem /* 18px */; + line-height: 1.75rem /* 28px */; + color: #374151; +} + +/* ===== Breakpoints ===== */ +@media (min-width: 640px) { + .desktop-header-container { + padding: 1.5rem 2rem; /* 24px 32px */ + } + + .hero-wrapper { + padding: 6rem 2rem; /* 96px 32px */ + } + + .hero-title { + font-size: 3.75rem /* 60px */; + line-height: 1; + } + + .category-section-wrapper { + padding: 2.5rem 1.5rem; /* 40px 24px */ + } + + .category-section-grid-wrapper { + grid-template-columns: repeat(2, minmax(0, 1fr)); + grid-template-rows: repeat(2, minmax(0, 1fr)); + column-gap: 1.5rem /* 24px */; + grid-row: span 2 / span 2; + } + + .category-section-grid-main { + aspect-ratio: 1 / 1; + grid-row: span 2 / span 2; + max-height: 100%; + } + + .category-section-grid-main-img { + object-position: center; + } + + .category-section-categories-container { + position: relative; + max-height: 300px; + } + + .category-section-categories-img { + position: absolute; + inset: 0px; + height: 100%; + width: 100%; + } + + .category-section-categories-cta-container { + position: absolute; + inset: 0px; + } + + .category-page-container { + padding: 0 1.5rem; /* 0 24px */ + } + + .products-list { + margin: 0; + } + + .product-card-container { + padding: 1.5rem; + } + + .back-button-wrapper { + padding-left: 1.5rem /* 24px */; + padding-right: 1.5rem /* 24px */; + } + + .product-page-wrapper { + padding: 2.5rem 1.5rem; /* 40px 24px */ + } + + .image-selector-wrapper { + display: block; + } + + .image-selector-main-img { + border-radius: 0.5rem /* 8px */; + } + + .product-page-info-container { + margin-top: 4rem /* 64px */; + padding-left: 0px; + padding-right: 0px; + } +} + +@media (min-width: 768px) { + footer { + margin-top: 5rem; /* 80px */ + } + + .mobile-header-wrapper { + display: none; + } + + .desktop-header-wrapper { + display: block; + } + + .logo-text { + font-size: 1.5rem /* 24px */; + line-height: 2rem /* 32px */; + } + + .footer-wrapper { + display: flex; + align-items: center; + justify-content: space-between; + padding: 3rem 2rem; /* 48px 32px */ + } + + .footer-links-container { + order: 2; + } + + .footer-copyright { + order: 1; + margin-top: 0; + } + + .products-list { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } +} + +@media (min-width: 1024px) { + footer { + margin-top: 6rem; /* 96px */ + } + + .category-section-wrapper { + padding: 2.5rem 2rem /* 40px 32px */; + } + + .category-page-container { + padding: 0 2rem; /* 0 32px */ + } + + .products-list { + grid-template-columns: repeat(4, minmax(0, 1fr)); + } + + .back-button-wrapper { + max-width: 80rem /* 1280px */; + padding-left: 2rem /* 32px */; + padding-right: 2rem /* 32px */; + } + + .product-page-wrapper { + padding: 2.5rem 2rem; /* 40px 32px */ + max-width: 80rem /* 1280px */; + } + + .product-page-container { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + align-items: center; + column-gap: 2rem /* 32px */; + } + + .image-selector-wrapper { + max-width: none; + } + + .product-page-info-container { + margin-top: 0px; + } +} diff --git a/06_js-debug/uebungen/u17-19_shop-vitest/src/lib/datatypes.ts b/06_js-debug/uebungen/u17-19_shop-vitest/src/lib/datatypes.ts new file mode 100644 index 0000000..f0e5483 --- /dev/null +++ b/06_js-debug/uebungen/u17-19_shop-vitest/src/lib/datatypes.ts @@ -0,0 +1,45 @@ +export interface Product { + id: number; + title: string; + description: string; + category: string; + price: number; + discountPercentage: number; + rating: number; + stock: number; + tags: string[]; + brand: string; + sku: string; + weight: number; + dimensions: Dimensions; + warrantyInformation: string; + shippingInformation: string; + availabilityStatus: string; + reviews: Review[]; + returnPolicy: string; + minimumOrderQuantity: number; + meta: Meta; + images: string[]; + thumbnail: string; +} + +export interface Dimensions { + width: number; + height: number; + depth: number; +} + +export interface Review { + rating: number; + comment: string; + date: Date; + reviewerName: string; + reviewerEmail: string; +} + +export interface Meta { + createdAt: Date; + updatedAt: Date; + barcode: string; + qrCode: string; +} diff --git a/06_js-debug/uebungen/u17-19_shop-vitest/src/lib/helpers.ts b/06_js-debug/uebungen/u17-19_shop-vitest/src/lib/helpers.ts new file mode 100644 index 0000000..593e340 --- /dev/null +++ b/06_js-debug/uebungen/u17-19_shop-vitest/src/lib/helpers.ts @@ -0,0 +1,16 @@ +import { Product } from './datatypes'; + +export const fetchProductsByCategory = async ( + category: string +): Promise => { + try { + const response = await fetch( + `https://dummyjson.com/products/category/${category}` + ); + const data = await response.json(); + return data.products; + } catch (error) { + console.error(`Failed to fetch products for category ${category}:`, error); + return []; + } +}; diff --git a/06_js-debug/uebungen/u17-19_shop-vitest/src/main.tsx b/06_js-debug/uebungen/u17-19_shop-vitest/src/main.tsx new file mode 100644 index 0000000..0f17abc --- /dev/null +++ b/06_js-debug/uebungen/u17-19_shop-vitest/src/main.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import './index.css'; + +import { createBrowserRouter, RouterProvider } from 'react-router-dom'; +import Homepage from './pages/root'; +import WomenProducts from './pages/women'; +import MenProducts from './pages/men'; +import AccessoryProducts from './pages/accessories'; +import ProductPage from './pages/product'; + +const router = createBrowserRouter([ + { + path: '/', + element: , + }, + { + path: '/women', + element: , + }, + { + path: '/men', + element: , + }, + { + path: '/accessories', + element: , + }, + { + path: '/products/:productId', + element: , + }, +]); + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + , +); diff --git a/06_js-debug/uebungen/u17-19_shop-vitest/src/pages/accessories.tsx b/06_js-debug/uebungen/u17-19_shop-vitest/src/pages/accessories.tsx new file mode 100644 index 0000000..d5e9033 --- /dev/null +++ b/06_js-debug/uebungen/u17-19_shop-vitest/src/pages/accessories.tsx @@ -0,0 +1,88 @@ +import { useState, useEffect } from 'react'; + +import Header from '../components/Header'; +import ProductList from '../components/ProductList'; +import Footer from '../components/Footer'; + +import { Product } from '../lib/datatypes'; +import { fetchProductsByCategory } from '../lib/helpers'; + +export default function AccessoryProducts() { + const [products, setProducts] = useState([]); + const [selectedCategory, setSelectedCategory] = useState('all'); + + const categories = [ + 'all', + 'mobile-accessories', + 'sports-accessories', + 'kitchen-accessories', + ]; + + const fetchProducts = async () => { + try { + const fetchedProducts = + selectedCategory === 'all' + ? await Promise.all(categories.slice(1).map(fetchProductsByCategory)) + : await fetchProductsByCategory(selectedCategory); + + const allProducts = Array.isArray(fetchedProducts) + ? fetchedProducts.flat() + : fetchedProducts; + + setProducts(allProducts); + } catch (error) { + console.error('Failed to fetch products:', error); + } + }; + + useEffect(() => { + fetchProducts(); + }, [selectedCategory]); + + return ( +
+
+ +
+
+
+

+ Accessories +

+ +
+ + +
+
+ +
+ {products && } +
+
+
+ +
+
+ ); +} diff --git a/06_js-debug/uebungen/u17-19_shop-vitest/src/pages/men.tsx b/06_js-debug/uebungen/u17-19_shop-vitest/src/pages/men.tsx new file mode 100644 index 0000000..710cc0b --- /dev/null +++ b/06_js-debug/uebungen/u17-19_shop-vitest/src/pages/men.tsx @@ -0,0 +1,83 @@ +import { useState, useEffect } from 'react'; + +import Header from '../components/Header'; +import ProductList from '../components/ProductList'; +import Footer from '../components/Footer'; + +import { Product } from '../lib/datatypes'; +import { fetchProductsByCategory } from '../lib/helpers'; + +export default function MenProducts() { + const [products, setProducts] = useState([]); + const [selectedCategory, setSelectedCategory] = useState('all'); + + const categories = ['all', 'mens-shirts', 'mens-shoes', 'mens-watches']; + + const fetchProducts = async () => { + try { + const fetchedProducts = + selectedCategory === 'all' + ? await Promise.all(categories.slice(1).map(fetchProductsByCategory)) + : await fetchProductsByCategory(selectedCategory); + + const allProducts = Array.isArray(fetchedProducts) + ? fetchedProducts.flat() + : fetchedProducts; + + setProducts(allProducts); + } catch (error) { + console.error('Failed to fetch products:', error); + } + }; + + useEffect(() => { + fetchProducts(); + }, [selectedCategory]); + + return ( +
+
+ +
+
+
+

+ Men Products +

+ +
+ + +
+
+ +
+ {products && } +
+
+
+ +
+
+ ); +} diff --git a/06_js-debug/uebungen/u17-19_shop-vitest/src/pages/product.tsx b/06_js-debug/uebungen/u17-19_shop-vitest/src/pages/product.tsx new file mode 100644 index 0000000..d90186f --- /dev/null +++ b/06_js-debug/uebungen/u17-19_shop-vitest/src/pages/product.tsx @@ -0,0 +1,188 @@ +import { useState, useEffect } from 'react'; +import { Product, Review } from '../lib/datatypes'; + +import { useParams, useNavigate } from 'react-router-dom'; + +import Header from '../components/Header'; +import ImageGallery from '../components/ImageGallery'; +import Footer from '../components/Footer'; + +const ProductPage = () => { + const { productId } = useParams<{ productId: string }>(); + const [product, setProduct] = useState(null); + + const fetchProduct = async () => { + try { + const productRes = await fetch(`https://dummyjson.com/products/${productId}`); + const productData = await productRes.json(); + setProduct(productData); + } catch (error) { + console.error('Failed to fetch products:', error); + } + }; + + useEffect(() => { + fetchProduct(); + }, [productId]); + + if (!product) { + return null; + } + + return ( +
+
+ +
+ + +
+
+ + + {/* Product info */} +
+

{product.title}

+ +
+

Product information

+ +

+ {Number(product.price).toLocaleString('en-US', { + style: 'currency', + currency: 'USD', + })} +

+ + {/* Rating */} +
+

Rating

+ +
+
+ {[0, 1, 2, 3, 4].map((rating) => ( + rating ? '#1e293b' : '#e5e7eb', + }} + aria-hidden="true" + /> + ))} +
+

{product.rating} out of 5 stars

+
+
+ + {/* Availability */} +
+

{product.stock} in stock

+
+ + {/* Description */} +
+

Description

+ +

{product.description}

+
+ + {/* Tags */} +
+

Tags

+
+ {product.tags.map((tag) => ( + + {tag} + + ))} +
+
+
+
+ + +
+
+
+ +
+
+ ); +}; + +export default ProductPage; + +function BackButton() { + const navigate = useNavigate(); + + return ( +
+ +
+ ); +} + +function Reviews({ reviews }: { reviews: Review[] }) { + return ( +
+

Reviews

+ +
+ {reviews.map((review, index) => ( +
+
+ {[0, 1, 2, 3, 4].map((rating) => ( + rating ? '#1e293b' : '#e5e7eb', + }} + aria-hidden="true" + /> + ))} +
+ +
+

{review.reviewerName}

+ +

{new Date(review.date).toLocaleDateString()}

+
+ +

{review.comment}

+
+ ))} +
+
+ ); +} + +function ChevronRightIcon(props: React.SVGProps) { + return ( + + + + ); +} + +function StarIcon(props: React.SVGProps) { + return ( + + + + ); +} diff --git a/06_js-debug/uebungen/u17-19_shop-vitest/src/pages/root.tsx b/06_js-debug/uebungen/u17-19_shop-vitest/src/pages/root.tsx new file mode 100644 index 0000000..cf40e02 --- /dev/null +++ b/06_js-debug/uebungen/u17-19_shop-vitest/src/pages/root.tsx @@ -0,0 +1,43 @@ +import Header from '../components/Header'; +import Hero from '../components/Hero'; +import CategorySection from '../components/CategorySection'; +import Footer from '../components/Footer'; + +const categories = [ + { + imgSrc: + 'https://images.unsplash.com/photo-1530011940847-04cba36df39f?q=80&w=1920', + imgAlt: + 'Two female models sitting on a bed. Photo by Billie (@billiebodybrand) on Unsplash.', + href: '/women', + linkText: 'Women Clothing', + }, + { + imgSrc: + 'https://images.unsplash.com/photo-1595909315417-2edd382a56dc?q=80&w=1920', + imgAlt: + 'Jumping rope, smartphone, sports bra, a pair of sneakers, and a water bottle. Photo by erica steeves on Unsplash.', + href: '/accessories', + linkText: 'Accessories', + }, + { + imgSrc: + 'https://images.unsplash.com/photo-1467779009031-53938b78ca38?q=80&w=1920', + imgAlt: 'Group of men sitting in a van. Photo by Luke Porter on Unsplash.', + href: '/men', + linkText: 'Men Clothing', + }, +]; + +export default function Root() { + return ( + <> +
+
+ + +
+