From 8043fe1eef1af799dce9527db8a01ac290a2cd7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robin=20Nederg=C3=A5rd?= Date: Thu, 20 Nov 2025 11:41:58 +0100 Subject: [PATCH] fix: prompting for web shop --- package-lock.json | 71 ++----- package.json | 4 +- src/App.css | 478 ++++++++++++++++++++++++++++++++++++++++--- src/App.test.tsx | 48 +++-- src/App.tsx | 404 ++++++++++++++++++++++++++++++------ src/data/products.ts | 105 ++++++++++ src/index.css | 75 +++---- src/main.tsx | 5 +- 8 files changed, 981 insertions(+), 209 deletions(-) create mode 100644 src/data/products.ts diff --git a/package-lock.json b/package-lock.json index 3357cc6..34b05ff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,8 +10,7 @@ "license": "ISC", "dependencies": { "react": "^19.2.0", - "react-dom": "^19.2.0", - "react-router-dom": "^7.9.6" + "react-dom": "^19.2.0" }, "devDependencies": { "@commitlint/cli": "^20.1.0", @@ -22,6 +21,7 @@ "@semantic-release/npm": "^13.1.2", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.0", + "@testing-library/user-event": "^14.6.1", "@types/node": "^24.10.1", "@types/react": "^19.2.6", "@types/react-dom": "^19.2.3", @@ -2913,6 +2913,20 @@ } } }, + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, "node_modules/@types/aria-query": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", @@ -4086,15 +4100,6 @@ "dev": true, "license": "MIT" }, - "node_modules/cookie": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", - "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -9738,44 +9743,6 @@ "node": ">=0.10.0" } }, - "node_modules/react-router": { - "version": "7.9.6", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.6.tgz", - "integrity": "sha512-Y1tUp8clYRXpfPITyuifmSoE2vncSME18uVLgaqyxh9H35JWpIfzHo+9y3Fzh5odk/jxPW29IgLgzcdwxGqyNA==", - "license": "MIT", - "dependencies": { - "cookie": "^1.0.1", - "set-cookie-parser": "^2.6.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "react": ">=18", - "react-dom": ">=18" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - } - } - }, - "node_modules/react-router-dom": { - "version": "7.9.6", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.9.6.tgz", - "integrity": "sha512-2MkC2XSXq6HjGcihnx1s0DBWQETI4mlis4Ux7YTLvP67xnGxCvq+BcCQSO81qQHVUTM1V53tl4iVVaY5sReCOA==", - "license": "MIT", - "dependencies": { - "react-router": "7.9.6" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "react": ">=18", - "react-dom": ">=18" - } - }, "node_modules/read-package-up": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/read-package-up/-/read-package-up-11.0.0.tgz", @@ -10571,12 +10538,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/set-cookie-parser": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", - "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", - "license": "MIT" - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", diff --git a/package.json b/package.json index ad7cb32..5b68bbd 100644 --- a/package.json +++ b/package.json @@ -16,8 +16,7 @@ }, "dependencies": { "react": "^19.2.0", - "react-dom": "^19.2.0", - "react-router-dom": "^7.9.6" + "react-dom": "^19.2.0" }, "devDependencies": { "@commitlint/cli": "^20.1.0", @@ -28,6 +27,7 @@ "@semantic-release/npm": "^13.1.2", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.0", + "@testing-library/user-event": "^14.6.1", "@types/node": "^24.10.1", "@types/react": "^19.2.6", "@types/react-dom": "^19.2.3", diff --git a/src/App.css b/src/App.css index b9d355d..cfd784a 100644 --- a/src/App.css +++ b/src/App.css @@ -1,42 +1,470 @@ -#root { - max-width: 1280px; +.shop { + max-width: 1200px; margin: 0 auto; - padding: 2rem; - text-align: center; + padding: 3rem clamp(1.25rem, 4vw, 3rem) 4rem; + display: flex; + flex-direction: column; + gap: 3rem; +} + +.hero { + background: radial-gradient(circle at top left, rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0.1)), + var(--card); + border-radius: 32px; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); + overflow: hidden; + box-shadow: 0 25px 80px rgba(15, 23, 42, 0.1); +} + +.hero__content { + padding: clamp(2rem, 5vw, 4rem); + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.hero__media img { + width: 100%; + height: 100%; + object-fit: cover; + min-height: 320px; + filter: saturate(1.05); +} + +.hero__eyebrow, +.eyebrow { + font-size: 0.85rem; + letter-spacing: 0.24em; + text-transform: uppercase; + color: var(--muted); +} + +.hero__lead { + font-size: 1.1rem; + color: var(--muted-strong); + max-width: 32ch; +} + +.hero__actions { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; +} + +.hero__meta { + display: flex; + gap: 1rem; + font-weight: 600; +} + +.btn { + border: none; + border-radius: 999px; + padding: 0.85rem 1.6rem; + font-size: 0.95rem; + font-weight: 600; + cursor: pointer; + transition: transform 200ms ease, box-shadow 200ms ease, background 200ms ease; +} + +.btn--primary { + background: var(--accent); + color: #fff; + box-shadow: 0 12px 24px rgba(241, 84, 53, 0.25); +} + +.btn--ghost { + background: transparent; + border: 1px solid rgba(15, 23, 42, 0.15); + color: var(--text); +} + +.btn:hover { + transform: translateY(-2px); +} + +.perks { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 1.25rem; +} + +.perks article { + background: var(--card); + padding: 1.5rem; + border-radius: 20px; + border: 1px solid var(--border); + min-height: 150px; +} + +.perks h3 { + margin-bottom: 0.35rem; +} + +.categories { + background: var(--card); + border-radius: 32px; + padding: clamp(2rem, 5vw, 3.5rem); + display: flex; + flex-direction: column; + gap: 2rem; + border: 1px solid var(--border); +} + +.section-heading { + max-width: 520px; +} + +.categories__grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); + gap: 1rem; +} + +.categories__grid article { + background: rgba(15, 23, 42, 0.04); + border-radius: 20px; + padding: 1.25rem; + min-height: 130px; + display: flex; + flex-direction: column; + justify-content: space-between; +} + +.product-grid { + display: flex; + flex-direction: column; + gap: 2.5rem; +} + +.product-grid__items { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); + gap: 2rem; +} + +.product-card { + background: var(--card); + border-radius: 28px; + overflow: hidden; + border: 1px solid var(--border); + display: flex; + flex-direction: column; + min-height: 100%; + transition: transform 200ms ease, box-shadow 200ms ease; +} + +.product-card__media { + position: relative; + overflow: hidden; + aspect-ratio: 4 / 3; +} + +.product-card__media img { + width: 100%; + height: 100%; + object-fit: cover; + transition: transform 300ms ease; +} + +.product-card:hover .product-card__media img { + transform: scale(1.04); +} + +.product-card__badge { + position: absolute; + top: 1rem; + left: 1rem; + background: rgba(0, 0, 0, 0.65); + color: #fff; + padding: 0.35rem 0.75rem; + border-radius: 999px; + font-size: 0.75rem; +} + +.product-card__body { + padding: 1.75rem; + display: flex; + flex-direction: column; + gap: 0.8rem; + flex: 1; +} + +.product-card__category { + text-transform: uppercase; + letter-spacing: 0.2em; + font-size: 0.7rem; + color: var(--muted); +} + +.product-card__description { + color: var(--muted-strong); + margin: 0; + flex: 1; +} + +.product-card__meta { + display: flex; + justify-content: space-between; + align-items: center; + font-weight: 600; +} + +.product-card__price { + font-size: 1.1rem; +} + +.product-card__colors { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + font-size: 0.85rem; + color: var(--muted-strong); } -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; +.product-card__cta { + align-self: flex-start; + border-radius: 999px; + border: none; + background: rgba(15, 23, 42, 0.85); + color: #fff; + padding: 0.55rem 1.4rem; + text-transform: uppercase; + letter-spacing: 0.08em; + font-size: 0.75rem; + transition: transform 200ms ease, box-shadow 200ms ease; } -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); + +.product-card--highlight { + animation: cardPulse 500ms ease; + box-shadow: 0 10px 20px rgba(241, 84, 53, 0.08); } -.logo.react:hover { - filter: drop-shadow(0 0 2em #61dafbaa); + +.product-card__cta--pulse { + animation: ctaPulse 500ms ease; + background: linear-gradient(120deg, rgba(241, 84, 53, 0.95), rgba(241, 84, 53, 0.8)); + box-shadow: 0 8px 18px rgba(241, 84, 53, 0.18); } -@keyframes logo-spin { - from { - transform: rotate(0deg); +@keyframes cardPulse { + 0% { + transform: scale(1); + opacity: 1; + } + 30% { + transform: scale(1.004); + opacity: 0.98; } - to { - transform: rotate(360deg); + 100% { + transform: scale(1); + opacity: 1; } } -@media (prefers-reduced-motion: no-preference) { - a:nth-of-type(2) .logo { - animation: logo-spin infinite 20s linear; +@keyframes ctaPulse { + 0% { + transform: scale(1); } + 35% { + transform: scale(1.03); + } + 100% { + transform: scale(1); + } +} + +.editorial { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); + gap: 2rem; + align-items: center; + background: var(--card); + border-radius: 32px; + padding: clamp(2rem, 5vw, 3.5rem); + border: 1px solid var(--border); +} + +.editorial__media img { + width: 100%; + border-radius: 24px; + object-fit: cover; + min-height: 320px; +} + +.editorial__content ul { + padding-left: 1.2rem; + color: var(--muted-strong); + line-height: 1.8; +} + +.editorial__content button { + margin-top: 1rem; +} + +.cart-bar { + display: flex; + justify-content: space-between; + align-items: center; + background: var(--card); + border-radius: 28px; + padding: 1.5rem 2rem; + border: 1px solid var(--border); +} + +.cart-toggle { + display: inline-flex; + align-items: center; + gap: 0.65rem; + background: #0f172a; + color: #fff; + border: none; + border-radius: 999px; + padding: 0.75rem 1.3rem; + font-weight: 600; + cursor: pointer; +} + +.cart-pill { + display: inline-flex; + justify-content: center; + align-items: center; + width: 32px; + height: 32px; + border-radius: 50%; + background: rgba(255, 255, 255, 0.15); +} + +.cart-panel { + background: var(--card); + border-radius: 32px; + border: 1px solid var(--border); + padding: clamp(1.5rem, 4vw, 3rem); + display: grid; + gap: 1.5rem; +} + +.cart-panel--open { + box-shadow: 0 20px 60px rgba(15, 23, 42, 0.12); +} + +.cart-panel__header { + display: flex; + justify-content: space-between; + align-items: center; +} + +.cart-panel__note { + color: var(--muted-strong); +} + +.cart-panel__empty { + padding: 1.5rem; + border-radius: 20px; + background: rgba(15, 23, 42, 0.04); + text-align: center; } -.card { - padding: 2em; +.cart-panel__lines { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 1rem; } -.read-the-docs { - color: #888; +.cart-line { + border-radius: 24px; + border: 1px solid var(--border); + padding: 1.25rem 1.5rem; + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.cart-line__info { + display: flex; + justify-content: space-between; + font-weight: 600; +} + +.cart-line__controls { + display: flex; + justify-content: space-between; + align-items: center; + gap: 1rem; +} + +.quantity { + display: inline-flex; + align-items: center; + border: 1px solid var(--border); + border-radius: 999px; + overflow: hidden; +} + +.quantity button { + border: none; + background: transparent; + padding: 0.35rem 0.9rem; + font-size: 1.2rem; + cursor: pointer; +} + +.quantity span { + min-width: 2rem; + text-align: center; + font-weight: 600; +} + +.cart-line__total { + font-weight: 600; +} + +.cart-panel__summary { + border-top: 1px solid var(--border); + padding-top: 1rem; + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.cart-panel__summary-row { + display: flex; + justify-content: space-between; + color: var(--muted-strong); +} + +.cart-panel__summary-row--total { + font-size: 1.1rem; + color: var(--text); + font-weight: 600; +} + +.btn--small { + padding: 0.5rem 1.1rem; + font-size: 0.85rem; +} + +.btn--full { + width: 100%; + text-align: center; +} + +@media (max-width: 720px) { + .hero__actions { + flex-direction: column; + align-items: flex-start; + } + + .cart-line__info, + .cart-line__controls { + flex-direction: column; + align-items: flex-start; + } + + .cart-panel { + padding: 1.5rem; + } } diff --git a/src/App.test.tsx b/src/App.test.tsx index afe527d..8b1dd6d 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -1,19 +1,43 @@ import { describe, it, expect } from 'vitest' +import '@testing-library/jest-dom/vitest' import { render, screen } from '@testing-library/react' -import { MemoryRouter } from 'react-router-dom' +import userEvent from '@testing-library/user-event' import App from './App' +import products from './data/products' describe('App', () => { - it('renders main navigation links', () => { - render( - - - , - ) - - expect(screen.getByRole('link', { name: /home/i })).not.toBeNull() - expect(screen.getByRole('link', { name: /about/i })).not.toBeNull() - expect(screen.getByRole('link', { name: /tabs/i })).not.toBeNull() + it('renders hero content and product cards', () => { + render() + + expect(screen.getByRole('heading', { name: /modern home shop/i })).toBeInTheDocument() + expect(screen.getAllByTestId('product-card')).toHaveLength(products.length) }) -}) + it('adds items to the basket and adjusts quantities', async () => { + const user = userEvent.setup() + render() + + const addButtons = screen.getAllByRole('button', { name: /add to bag/i }) + await user.click(addButtons[0]) + + const count = screen.getByTestId('cart-count') + expect(count).toHaveTextContent('1') + + await user.click(screen.getByTestId('cart-toggle')) + + const firstProduct = products[0] + const increaseButton = screen.getByRole('button', { + name: new RegExp(`Increase quantity for ${firstProduct.name}`, 'i'), + }) + const decreaseButton = screen.getByRole('button', { + name: new RegExp(`Decrease quantity for ${firstProduct.name}`, 'i'), + }) + const quantityDisplay = screen.getByTestId(`cart-qty-${firstProduct.id}`) + + await user.click(increaseButton) + expect(quantityDisplay).toHaveTextContent('2') + + await user.click(decreaseButton) + expect(quantityDisplay).toHaveTextContent('1') + }) +}) diff --git a/src/App.tsx b/src/App.tsx index c348c5d..22bb1f2 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,86 +1,364 @@ -import { useState } from 'react' -import { Link, Route, Routes } from 'react-router-dom' +import { useEffect, useMemo, useRef, useState } from 'react' import './App.css' +import products, { type Product } from './data/products' -function App() { - return ( -
-
-

React Router + Vite + TypeScript

- -
-
- - } /> - } /> - } /> - -
-
- ) +const currency = new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD', +}) + +const perks = [ + { title: 'Express shipping', detail: 'Complimentary on orders over $150' }, + { title: '30-day trial', detail: 'Live with every piece before you decide' }, + { title: 'Design support', detail: 'Chat with stylists for pairing advice' }, +] + +const categorySummaries = Object.entries( + products.reduce>((acc, product) => { + acc[product.category] = (acc[product.category] ?? 0) + 1 + return acc + }, {}), +) + .map(([category, count]) => ({ category, count })) + .sort((a, b) => b.count - a.count) + +const heroProduct = products[0] +const editorialHighlight = products.find((product) => product.badge === 'Limited') ?? heroProduct +const freeShippingThreshold = 150 +const flatShippingRate = 15 +const productDictionary = products.reduce>((acc, product) => { + acc[product.id] = product + return acc +}, {}) + +type CartLineItem = { + product: Product + quantity: number } -function HomePage() { +function ProductCard({ + product, + onAdd, + isHighlighted, +}: { + product: Product + onAdd: () => void + isHighlighted: boolean +}) { return ( -
-

Home

-

This is the home page.

-
+
+
+ {product.name} + {product.badge && {product.badge}} +
+
+

{product.category}

+

{product.name}

+

{product.description}

+
+ {currency.format(product.price)} + ★ {product.rating.toFixed(1)} +
+
+ {product.colors.map((color) => ( + {color} + ))} +
+ +
+
) } -function AboutPage() { +function CartLine({ + item, + onIncrement, + onDecrement, +}: { + item: CartLineItem + onIncrement: () => void + onDecrement: () => void +}) { return ( -
-

About

-

- This starter uses React Router, TypeScript, Vitest, ESLint, and Prettier configured for a - modern workflow. -

-
+
  • +
    +

    {item.product.name}

    + {currency.format(item.product.price)} +
    +
    +
    + + + {item.quantity} + + +
    + + {currency.format(item.product.price * item.quantity)} + +
    +
  • ) } -function TabsPage() { - const [activeTab, setActiveTab] = useState<'red' | 'green' | 'blue'>('red') +function App() { + const [cart, setCart] = useState>({}) + const [isCartOpen, setIsCartOpen] = useState(false) + const [highlightedProduct, setHighlightedProduct] = useState<{ + id: string + token: number + } | null>(null) + const highlightTimeoutRef = useRef(null) + const highlightSequenceRef = useRef(0) + + const cartItems = useMemo(() => { + return Object.entries(cart) + .map(([productId, quantity]) => { + const product = productDictionary[productId] + if (!product) return null + return { product, quantity } + }) + .filter(Boolean) as CartLineItem[] + }, [cart]) + + const cartCount = useMemo( + () => cartItems.reduce((total, item) => total + item.quantity, 0), + [cartItems], + ) + const subtotal = useMemo( + () => cartItems.reduce((total, item) => total + item.product.price * item.quantity, 0), + [cartItems], + ) + const shipping = subtotal === 0 || subtotal >= freeShippingThreshold ? 0 : flatShippingRate + const total = subtotal + shipping + const freeShippingMessage = + subtotal === 0 + ? 'Start building your bag to unlock complimentary delivery.' + : shipping === 0 + ? 'Shipping is on us today.' + : `Add ${currency.format(freeShippingThreshold - subtotal)} more for free express delivery.` + + const updateQuantity = (productId: string, updater: (current: number) => number) => { + setCart((current) => { + const nextQuantity = updater(current[productId] ?? 0) + if (nextQuantity <= 0) { + const rest = { ...current } + delete rest[productId] + return rest + } + return { ...current, [productId]: nextQuantity } + }) + } + + const addToCart = (productId: string) => { + updateQuantity(productId, (current) => current + 1) + const token = ++highlightSequenceRef.current + setHighlightedProduct({ id: productId, token }) + if (highlightTimeoutRef.current) { + clearTimeout(highlightTimeoutRef.current) + } + highlightTimeoutRef.current = window.setTimeout(() => { + setHighlightedProduct((currentHighlight) => + currentHighlight?.token === token ? null : currentHighlight, + ) + }, 900) + } - const backgroundColor = activeTab - const text = activeTab.toUpperCase() + useEffect(() => { + return () => { + if (highlightTimeoutRef.current) { + clearTimeout(highlightTimeoutRef.current) + } + } + }, []) + + const toggleCart = () => setIsCartOpen((open) => !open) return ( -
    -

    Color Tabs

    -
    +
    +
    +
    +

    Your bag

    +

    {cartCount ? `${cartCount} item${cartCount > 1 ? 's' : ''}` : 'No items yet'}

    +
    - - -
    -
    -

    {text}

    -
    + + {isCartOpen && ( +
    +
    +
    +

    Shopping bag

    +

    Ready to ship

    +
    + +
    +

    {freeShippingMessage}

    + {cartItems.length === 0 ? ( +

    Your basket is empty – add your favorite finds.

    + ) : ( +
      + {cartItems.map((item) => ( + updateQuantity(item.product.id, (qty) => qty + 1)} + onDecrement={() => updateQuantity(item.product.id, (qty) => qty - 1)} + /> + ))} +
    + )} +
    +
    + Subtotal + {currency.format(subtotal)} +
    +
    + Shipping + {shipping === 0 ? 'Complimentary' : currency.format(shipping)} +
    +
    + Total + {currency.format(total)} +
    + +
    +
    + )} + +
    +
    +

    New season edit

    +

    Meet the modern home shop

    +

    + Curated furniture, lighting, and objects crafted in small batches and ready to ship. +

    +
    + + Shop the collection + + +
    +
    + {currency.format(heroProduct.price)} + {heroProduct.name} +
    +
    +
    + {heroProduct.name} +
    +
    + +
    + {perks.map((perk) => ( +
    +

    {perk.title}

    +

    {perk.detail}

    +
    + ))} +
    + +
    +
    +

    Shop by room

    +

    Spaces with intention

    +

    Refresh a single corner or rethink your whole home with designer-backed palettes.

    +
    +
    + {categorySummaries.map((category) => ( +
    +

    {category.category}

    +

    {category.count} curated pieces

    +
    + ))} +
    +
    + +
    +
    +

    Featured pieces

    +

    Crafted to layer beautifully

    +

    + Mix tactile fabrics, natural woods, and sculptural silhouettes for your signature look. +

    +
    +
    + {products.map((product) => ( + addToCart(product.id)} + isHighlighted={highlightedProduct?.id === product.id} + /> + ))} +
    +
    + +
    +
    + {editorialHighlight.name} +
    +
    +

    From the studio

    +

    Layered neutrals, elevated silhouettes

    +

    + We partner with small-batch workshops to produce timeless staples. Every stitch, weave, + and finishing touch is considered so you can style once and enjoy for years. +

    +
      +
    • Responsibly sourced materials and certified woods
    • +
    • Color stories developed with interior stylists
    • +
    • Transparent pricing and limited runs per season
    • +
    + +
    +
    + ) } diff --git a/src/data/products.ts b/src/data/products.ts new file mode 100644 index 0000000..ee0799d --- /dev/null +++ b/src/data/products.ts @@ -0,0 +1,105 @@ +export type Product = { + id: string + name: string + category: string + price: number + image: string + description: string + badge?: string + featured?: boolean + rating: number + colors: string[] +} + +const products: Product[] = [ + { + id: 'aurora-vase', + name: 'Aurora Porcelain Vase', + category: 'Decor', + price: 120, + image: 'https://images.unsplash.com/photo-1487412720507-e7ab37603c6f?auto=format&fit=crop&w=900&q=80', + description: 'Hand-thrown porcelain with a matte glaze inspired by slow dawns and fresh blooms.', + badge: 'New Arrival', + featured: true, + rating: 4.8, + colors: ['Cloud White', 'Soft Blush'], + }, + { + id: 'linen-set', + name: 'Coastal Linen Sheet Set', + category: 'Bedroom', + price: 240, + image: 'https://images.unsplash.com/photo-1505691938895-1758d7feb511?auto=format&fit=crop&w=900&q=80', + description: 'Stone-washed European flax linen that grows softer with every night.', + badge: 'Bestseller', + featured: true, + rating: 4.9, + colors: ['Mist', 'Sage', 'Sand'], + }, + { + id: 'atelier-mug', + name: 'Atelier Ceramic Mug Set', + category: 'Kitchen', + price: 64, + image: 'https://images.unsplash.com/photo-1488998527040-85054a85150e?auto=format&fit=crop&w=900&q=80', + description: 'Set of four wheel-thrown mugs with raw stoneware base and satin glaze.', + rating: 4.7, + colors: ['Charcoal', 'Oat'], + }, + { + id: 'soho-chair', + name: 'Soho Lounge Chair', + category: 'Furniture', + price: 890, + image: 'https://images.unsplash.com/photo-1493663284031-b7e3aefcae8e?auto=format&fit=crop&w=900&q=80', + description: 'Low-profile lounge chair upholstered in recycled bouclé with walnut legs.', + badge: 'Limited', + featured: true, + rating: 4.6, + colors: ['Ivory', 'Ink'], + }, + { + id: 'balance-lamp', + name: 'Balance Arc Lamp', + category: 'Lighting', + price: 310, + image: 'https://images.unsplash.com/photo-1481277542470-605612bd2d61?auto=format&fit=crop&w=900&q=80', + description: 'Sculptural floor lamp with adjustable arc and warm-diffused LED glow.', + rating: 4.5, + colors: ['Matte Black', 'Brass'], + }, + { + id: 'atelier-rug', + name: 'Atlas Wool Rug', + category: 'Living Room', + price: 620, + image: 'https://images.unsplash.com/photo-1501045661006-fcebe0257c3f?auto=format&fit=crop&w=900&q=80', + description: 'Hand-loomed Moroccan inspired rug in a low-contrast geometric pattern.', + rating: 4.8, + colors: ['Natural', 'Noir'], + }, + { + id: 'glassware-set', + name: 'Gradient Glassware Duo', + category: 'Kitchen', + price: 78, + image: 'https://images.unsplash.com/photo-1481349518771-20055b2a7b24?auto=format&fit=crop&w=900&q=80', + description: 'Heat-resistant borosilicate glasses with a subtle ombré finish.', + badge: 'Editors’ pick', + rating: 4.4, + colors: ['Amber Fade', 'Rose Fade'], + }, + { + id: 'planter-set', + name: 'Elevate Planter Trio', + category: 'Outdoor', + price: 185, + image: 'https://images.unsplash.com/photo-1449247709967-d4461a6a6103?auto=format&fit=crop&w=900&q=80', + description: 'Powder-coated steel planters with hidden drainage for effortless greenery.', + rating: 4.6, + colors: ['Eucalyptus', 'Clay'], + }, +] + +export default products + diff --git a/src/index.css b/src/index.css index 08a3ac9..615ab8a 100644 --- a/src/index.css +++ b/src/index.css @@ -1,68 +1,47 @@ :root { - font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; - line-height: 1.5; + font-family: 'Inter', 'Soehne', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + line-height: 1.6; font-weight: 400; - - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #242424; - + color: #0f172a; + background-color: #f7f4ef; + --text: #0f172a; + --muted: #94a3b8; + --muted-strong: #475569; + --card: #fff; + --border: rgba(15, 23, 42, 0.08); + --accent: #f15435; font-synthesis: none; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } -a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; -} -a:hover { - color: #535bf2; +*, +*::before, +*::after { + box-sizing: border-box; } body { margin: 0; - display: flex; - place-items: center; - min-width: 320px; min-height: 100vh; + background: #f7f4ef; } -h1 { - font-size: 3.2em; - line-height: 1.1; +h1, +h2, +h3, +h4, +h5, +h6 { + font-weight: 600; + margin: 0; } -button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - background-color: #1a1a1a; - cursor: pointer; - transition: border-color 0.25s; -} -button:hover { - border-color: #646cff; -} -button:focus, -button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; +p { + margin: 0; } -@media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; - } - a:hover { - color: #747bff; - } - button { - background-color: #f9f9f9; - } +a { + color: inherit; } diff --git a/src/main.tsx b/src/main.tsx index ade9d64..bef5202 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,13 +1,10 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' -import { BrowserRouter } from 'react-router-dom' import './index.css' import App from './App.tsx' createRoot(document.getElementById('root')!).render( - - - + , )