From 89046e6a40ee41e203db9927f3d57a45f4c0a11c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robin=20Nederg=C3=A5rd?= Date: Wed, 26 Nov 2025 10:52:42 +0100 Subject: [PATCH 1/2] feat: use tailwind for styling and refactor components --- package-lock.json | 702 ++++++++- package.json | 4 + postcss.config.js | 7 + src/App.css | 1264 ----------------- src/App.tsx | 203 +-- src/CheckoutPage.tsx | 246 +--- src/CheckoutSuccessPage.tsx | 138 +- src/LoginPage.tsx | 105 +- src/ProductCard.tsx | 86 +- src/ProductPage.tsx | 138 +- src/ProductsPage.tsx | 29 +- src/RegisterPage.tsx | 163 +-- src/Reviews.tsx | 229 +-- src/UserOrdersPage.tsx | 199 +-- src/components/BackLink.tsx | 20 + src/components/EmptyState.tsx | 50 + src/components/ErrorMessage.tsx | 13 + src/components/FormField.tsx | 62 + src/components/LoadingState.tsx | 19 + src/components/PageContainer.tsx | 28 + src/components/checkout/EmptyCartState.tsx | 16 + src/components/checkout/OrderSummary.tsx | 53 + .../checkout/PaymentMethodSelect.tsx | 32 + .../checkout/ShippingAddressForm.tsx | 94 ++ src/components/orders/OrderCard.tsx | 75 + src/components/orders/StatusBadge.tsx | 28 + src/components/product/AddToCartButton.tsx | 48 + src/components/product/ColorSelector.tsx | 43 + src/components/product/ProductDetails.tsx | 59 + src/components/product/ProductImage.tsx | 20 + src/components/product/ProductMeta.tsx | 32 + src/components/reviews/ReviewForm.tsx | 133 ++ src/components/reviews/ReviewItem.tsx | 41 + src/components/reviews/StarRating.tsx | 57 + src/index.css | 79 +- tailwind.config.js | 12 + 36 files changed, 2133 insertions(+), 2394 deletions(-) create mode 100644 postcss.config.js delete mode 100644 src/App.css create mode 100644 src/components/BackLink.tsx create mode 100644 src/components/EmptyState.tsx create mode 100644 src/components/ErrorMessage.tsx create mode 100644 src/components/FormField.tsx create mode 100644 src/components/LoadingState.tsx create mode 100644 src/components/PageContainer.tsx create mode 100644 src/components/checkout/EmptyCartState.tsx create mode 100644 src/components/checkout/OrderSummary.tsx create mode 100644 src/components/checkout/PaymentMethodSelect.tsx create mode 100644 src/components/checkout/ShippingAddressForm.tsx create mode 100644 src/components/orders/OrderCard.tsx create mode 100644 src/components/orders/StatusBadge.tsx create mode 100644 src/components/product/AddToCartButton.tsx create mode 100644 src/components/product/ColorSelector.tsx create mode 100644 src/components/product/ProductDetails.tsx create mode 100644 src/components/product/ProductImage.tsx create mode 100644 src/components/product/ProductMeta.tsx create mode 100644 src/components/reviews/ReviewForm.tsx create mode 100644 src/components/reviews/ReviewItem.tsx create mode 100644 src/components/reviews/StarRating.tsx create mode 100644 tailwind.config.js diff --git a/package-lock.json b/package-lock.json index 2c7861b..7ee3d55 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "@semantic-release/changelog": "^6.0.3", "@semantic-release/git": "^10.0.1", "@semantic-release/npm": "^13.1.2", + "@tailwindcss/postcss": "^4.1.17", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.6.1", @@ -31,6 +32,7 @@ "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.1.1", "@vitest/ui": "^4.0.10", + "autoprefixer": "^10.4.22", "eslint": "^9.39.1", "eslint-config-prettier": "^10.1.8", "eslint-plugin-react-hooks": "^7.0.1", @@ -39,8 +41,10 @@ "happy-dom": "^20.0.10", "husky": "^9.1.7", "jsdom": "^27.2.0", + "postcss": "^8.5.6", "prettier": "^3.6.2", "semantic-release": "^25.0.2", + "tailwindcss": "^4.1.17", "tsx": "^4.20.6", "typescript": "~5.9.3", "typescript-eslint": "^8.47.0", @@ -101,6 +105,19 @@ "dev": true, "license": "MIT" }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@apollo/cache-control-types": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@apollo/cache-control-types/-/cache-control-types-1.0.3.tgz", @@ -493,6 +510,7 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -1130,6 +1148,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -1173,6 +1192,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -2026,6 +2046,7 @@ "integrity": "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.3", @@ -3272,6 +3293,277 @@ "dev": true, "license": "MIT" }, + "node_modules/@tailwindcss/node": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.17.tgz", + "integrity": "sha512-csIkHIgLb3JisEFQ0vxr2Y57GUNYh447C8xzwj89U/8fdW8LhProdxvnVH6U8M2Y73QKiTIH+LWbK3V2BBZsAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.6.1", + "lightningcss": "1.30.2", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.17" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.17.tgz", + "integrity": "sha512-F0F7d01fmkQhsTjXezGBLdrl1KresJTcI3DB8EkScCldyKp3Msz4hub4uyYaVnk88BAS1g5DQjjF6F5qczheLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.17", + "@tailwindcss/oxide-darwin-arm64": "4.1.17", + "@tailwindcss/oxide-darwin-x64": "4.1.17", + "@tailwindcss/oxide-freebsd-x64": "4.1.17", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.17", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.17", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.17", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.17", + "@tailwindcss/oxide-linux-x64-musl": "4.1.17", + "@tailwindcss/oxide-wasm32-wasi": "4.1.17", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.17", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.17" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.17.tgz", + "integrity": "sha512-BMqpkJHgOZ5z78qqiGE6ZIRExyaHyuxjgrJ6eBO5+hfrfGkuya0lYfw8fRHG77gdTjWkNWEEm+qeG2cDMxArLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.17.tgz", + "integrity": "sha512-EquyumkQweUBNk1zGEU/wfZo2qkp/nQKRZM8bUYO0J+Lums5+wl2CcG1f9BgAjn/u9pJzdYddHWBiFXJTcxmOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.17.tgz", + "integrity": "sha512-gdhEPLzke2Pog8s12oADwYu0IAw04Y2tlmgVzIN0+046ytcgx8uZmCzEg4VcQh+AHKiS7xaL8kGo/QTiNEGRog==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.17.tgz", + "integrity": "sha512-hxGS81KskMxML9DXsaXT1H0DyA+ZBIbyG/sSAjWNe2EDl7TkPOBI42GBV3u38itzGUOmFfCzk1iAjDXds8Oh0g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.17.tgz", + "integrity": "sha512-k7jWk5E3ldAdw0cNglhjSgv501u7yrMf8oeZ0cElhxU6Y2o7f8yqelOp3fhf7evjIS6ujTI3U8pKUXV2I4iXHQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.17.tgz", + "integrity": "sha512-HVDOm/mxK6+TbARwdW17WrgDYEGzmoYayrCgmLEw7FxTPLcp/glBisuyWkFz/jb7ZfiAXAXUACfyItn+nTgsdQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.17.tgz", + "integrity": "sha512-HvZLfGr42i5anKtIeQzxdkw/wPqIbpeZqe7vd3V9vI3RQxe3xU1fLjss0TjyhxWcBaipk7NYwSrwTwK1hJARMg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.17.tgz", + "integrity": "sha512-M3XZuORCGB7VPOEDH+nzpJ21XPvK5PyjlkSFkFziNHGLc5d6g3di2McAAblmaSUNl8IOmzYwLx9NsE7bplNkwQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.17.tgz", + "integrity": "sha512-k7f+pf9eXLEey4pBlw+8dgfJHY4PZ5qOUFDyNf7SI6lHjQ9Zt7+NcscjpwdCEbYi6FI5c2KDTDWyf2iHcCSyyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.17.tgz", + "integrity": "sha512-cEytGqSSoy7zK4JRWiTCx43FsKP/zGr0CsuMawhH67ONlH+T79VteQeJQRO/X7L0juEUA8ZyuYikcRBf0vsxhg==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.6.0", + "@emnapi/runtime": "^1.6.0", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.0.7", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.17.tgz", + "integrity": "sha512-JU5AHr7gKbZlOGvMdb4722/0aYbU+tN6lv1kONx0JK2cGsh7g148zVWLM0IKR3NeKLv+L90chBVYcJ8uJWbC9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.17.tgz", + "integrity": "sha512-SKWM4waLuqx0IH+FMDUw6R66Hu4OuTALFgnleKbqhgGU30DY20NORZMZUKgLRjQXNN2TLzKvh48QXTig4h4bGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.17.tgz", + "integrity": "sha512-+nKl9N9mN5uJ+M7dBOOCzINw94MPstNR/GtIhz1fpZysxL/4a+No64jCBD6CPN+bIHWFx3KWuu8XJRrj/572Dw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.1.17", + "@tailwindcss/oxide": "4.1.17", + "postcss": "^8.4.41", + "tailwindcss": "4.1.17" + } + }, "node_modules/@testing-library/dom": { "version": "10.4.1", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", @@ -3367,8 +3659,7 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -3469,6 +3760,7 @@ "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -3486,6 +3778,7 @@ "integrity": "sha512-p/jUvulfgU7oKtj6Xpk8cA2Y1xKTtICGpJYeJXz2YVO2UcvjQgeRMLDGfDeqeRW2Ta+0QNFwcc8X3GH8SxZz6w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -3496,6 +3789,7 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "dev": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -3553,6 +3847,7 @@ "integrity": "sha512-lJi3PfxVmo0AkEY93ecfN+r8SofEqZNGByvHAI3GBLrvt1Cw6H5k1IM02nSzu0RfUafr2EvFSw0wAsZgubNplQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.47.0", "@typescript-eslint/types": "8.47.0", @@ -3889,6 +4184,7 @@ "integrity": "sha512-oWtNM89Np+YsQO3ttT5i1Aer/0xbzQzp66NzuJn/U16bB7MnvSzdLKXgk1kkMLYyKSSzA2ajzqMkYheaE9opuQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/utils": "4.0.10", "fflate": "^0.8.2", @@ -3985,6 +4281,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4075,7 +4372,6 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -4140,6 +4436,44 @@ "retry": "0.13.1" } }, + "node_modules/autoprefixer": { + "version": "10.4.22", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.22.tgz", + "integrity": "sha512-ARe0v/t9gO28Bznv6GgqARmVqcWOV3mfgUPn9becPHMiD3o9BwlRgaeccZnwTpZ7Zwqrm+c1sUSsMxIzQzc8Xg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.27.0", + "caniuse-lite": "^1.0.30001754", + "fraction.js": "^5.3.4", + "normalize-range": "^0.1.2", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -4260,6 +4594,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", @@ -4745,6 +5080,7 @@ "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", @@ -4997,6 +5333,16 @@ "node": ">=6" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -5015,8 +5361,7 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/dot-prop": { "version": "5.3.0", @@ -5091,6 +5436,20 @@ "node": ">= 0.8" } }, + "node_modules/enhanced-resolve": { + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/entities": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", @@ -5409,6 +5768,7 @@ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -6037,6 +6397,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, "node_modules/from2": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", @@ -6312,6 +6686,7 @@ "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.12.0.tgz", "integrity": "sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==", "license": "MIT", + "peer": true, "engines": { "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } @@ -7125,6 +7500,267 @@ "node": ">= 0.8.0" } }, + "node_modules/lightningcss": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", + "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.30.2", + "lightningcss-darwin-arm64": "1.30.2", + "lightningcss-darwin-x64": "1.30.2", + "lightningcss-freebsd-x64": "1.30.2", + "lightningcss-linux-arm-gnueabihf": "1.30.2", + "lightningcss-linux-arm64-gnu": "1.30.2", + "lightningcss-linux-arm64-musl": "1.30.2", + "lightningcss-linux-x64-gnu": "1.30.2", + "lightningcss-linux-x64-musl": "1.30.2", + "lightningcss-win32-arm64-msvc": "1.30.2", + "lightningcss-win32-x64-msvc": "1.30.2" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", + "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", + "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", + "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", + "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", + "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", + "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", + "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", + "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", + "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", + "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", + "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -7324,7 +7960,6 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -7363,6 +7998,7 @@ "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", "dev": true, "license": "MIT", + "peer": true, "bin": { "marked": "bin/marked.js" }, @@ -7673,6 +8309,16 @@ "node": "^16.14.0 || >=18.0.0" } }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/normalize-url": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.1.0.tgz", @@ -9944,6 +10590,7 @@ "dev": true, "inBundle": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -10645,6 +11292,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -10654,6 +11302,13 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -10686,7 +11341,6 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -10866,6 +11520,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -10875,6 +11530,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -10887,8 +11543,7 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/react-refresh": { "version": "0.18.0", @@ -11211,6 +11866,7 @@ "integrity": "sha512-6qGjWccl5yoyugHt3jTgztJ9Y0JVzyH8/Voc/D8PlLat9pwxQYXz7W1Dpnq5h0/G5GCYGUaDSlYcyk3AMh5A6g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@semantic-release/commit-analyzer": "^13.0.1", "@semantic-release/error": "^4.0.0", @@ -12332,6 +12988,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/tailwindcss": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz", + "integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/temp-dir": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-3.0.0.tgz", @@ -12515,6 +13192,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -12688,6 +13366,7 @@ "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" @@ -12772,6 +13451,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -13000,6 +13680,7 @@ "integrity": "sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -13093,6 +13774,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -13106,6 +13788,7 @@ "integrity": "sha512-2Fqty3MM9CDwOVet/jaQalYlbcjATZwPYGcqpiYQqgQ/dLC7GuHdISKgTYIVF/kaishKxLzleKWWfbSDklyIKg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/expect": "4.0.10", "@vitest/mocker": "4.0.10", @@ -13497,6 +14180,7 @@ "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", "dev": true, "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index 9a97ed7..d920e14 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "@semantic-release/changelog": "^6.0.3", "@semantic-release/git": "^10.0.1", "@semantic-release/npm": "^13.1.2", + "@tailwindcss/postcss": "^4.1.17", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.6.1", @@ -38,6 +39,7 @@ "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.1.1", "@vitest/ui": "^4.0.10", + "autoprefixer": "^10.4.22", "eslint": "^9.39.1", "eslint-config-prettier": "^10.1.8", "eslint-plugin-react-hooks": "^7.0.1", @@ -46,8 +48,10 @@ "happy-dom": "^20.0.10", "husky": "^9.1.7", "jsdom": "^27.2.0", + "postcss": "^8.5.6", "prettier": "^3.6.2", "semantic-release": "^25.0.2", + "tailwindcss": "^4.1.17", "tsx": "^4.20.6", "typescript": "~5.9.3", "typescript-eslint": "^8.47.0", diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..fb8e0e3 --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,7 @@ +export default { + plugins: { + '@tailwindcss/postcss': {}, + autoprefixer: {}, + }, +} + diff --git a/src/App.css b/src/App.css deleted file mode 100644 index 2479708..0000000 --- a/src/App.css +++ /dev/null @@ -1,1264 +0,0 @@ -.shop { - max-width: 1200px; - margin: 0 auto; - 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; -} - -.products-page { - max-width: 1400px; - margin: 0 auto; - padding: 1rem 2rem 2rem; -} - -.product-grid { - display: flex; - flex-direction: column; - gap: 2.5rem; -} - -.product-grid__items { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); - gap: 2rem; -} - -@media (min-width: 1200px) { - .product-grid__items { - grid-template-columns: repeat(4, 1fr); - } -} - -@media (min-width: 900px) and (max-width: 1199px) { - .product-grid__items { - grid-template-columns: repeat(3, 1fr); - } -} - -.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; - text-decoration: none; - color: inherit; - cursor: pointer; -} - -.product-card:hover { - transform: translateY(-2px); - box-shadow: 0 12px 32px rgba(15, 23, 42, 0.1); -} - -.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); -} - -.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; - display: inline-flex; - align-items: center; - gap: 0.5rem; -} - -.product-card__cta--highlight { - animation: buttonColorFade 1200ms ease-out forwards; -} - -.product-card--highlight { - animation: cardPulse 500ms ease; - box-shadow: 0 10px 20px rgba(241, 84, 53, 0.08); -} - -.product-card__checkmark { - display: inline-flex; - align-items: center; - animation: checkmarkFade 1200ms ease-out forwards; - margin-right: -0.25rem; -} - -.product-card__checkmark svg { - width: 16px; - height: 16px; - stroke: currentColor; -} - -@keyframes cardPulse { - 0% { - transform: scale(1); - opacity: 1; - } - 30% { - transform: scale(1.004); - opacity: 0.98; - } - 100% { - transform: scale(1); - opacity: 1; - } -} - -@keyframes checkmarkFade { - 0% { - opacity: 0; - transform: translateX(-4px) scale(0.8); - } - 20% { - opacity: 1; - transform: translateX(0) scale(1); - } - 70% { - opacity: 1; - transform: translateX(0) scale(1); - } - 100% { - opacity: 0; - transform: translateX(4px) scale(0.8); - } -} - -@keyframes buttonColorFade { - 0% { - background-color: rgba(15, 23, 42, 0.85); - } - 15% { - background-color: rgba(34, 197, 94, 0.9); - } - 50% { - background-color: rgba(34, 197, 94, 0.9); - } - 70% { - background-color: rgba(34, 197, 94, 0.75); - } - 85% { - background-color: rgba(34, 197, 94, 0.5); - } - 100% { - background-color: rgba(15, 23, 42, 0.85); - } -} - -.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; -} - -.cart-panel__lines { - list-style: none; - margin: 0; - padding: 0; - display: flex; - flex-direction: column; - gap: 1rem; -} - -.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; -} - -.cart-line__quantity-group { - display: flex; - align-items: center; - gap: 0.75rem; -} - -.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__remove { - border: none; - background: transparent; - color: var(--muted-strong); - cursor: pointer; - padding: 0.5rem; - display: inline-flex; - align-items: center; - justify-content: center; - border-radius: 8px; - transition: - color 200ms ease, - background-color 200ms ease, - transform 200ms ease; -} - -.cart-line__remove:hover { - color: var(--accent); - background-color: rgba(241, 84, 53, 0.1); - transform: scale(1.1); -} - -.cart-line__remove:active { - transform: scale(0.95); -} - -.cart-line__remove svg { - width: 16px; - height: 16px; - stroke: currentColor; -} - -.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; -} - -.product-page { - max-width: 1400px; - margin: 0 auto; - padding: 2rem; -} - -.product-page__back { - display: inline-flex; - align-items: center; - gap: 0.5rem; - color: var(--muted-strong); - text-decoration: none; - margin-bottom: 2rem; - font-size: 0.9rem; - transition: color 200ms ease; -} - -.product-page__back:hover { - color: var(--text); -} - -.product-page__content { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); - gap: 3rem; - align-items: start; -} - -.product-page__media { - position: relative; - border-radius: 32px; - overflow: hidden; - aspect-ratio: 4 / 3; - background: var(--card); - border: 1px solid var(--border); -} - -.product-page__media img { - width: 100%; - height: 100%; - object-fit: cover; -} - -.product-page__details { - display: flex; - flex-direction: column; - gap: 1.5rem; -} - -.product-page__category { - text-transform: uppercase; - letter-spacing: 0.2em; - font-size: 0.75rem; - color: var(--muted); - margin: 0; -} - -.product-page__title { - font-size: clamp(2rem, 5vw, 3rem); - line-height: 1.2; - margin: 0; -} - -.product-page__description { - font-size: 1.1rem; - line-height: 1.7; - color: var(--muted-strong); - margin: 0; -} - -.product-page__meta { - display: flex; - align-items: center; - gap: 1.5rem; - font-size: 1.5rem; - font-weight: 600; -} - -.product-page__price { - color: var(--text); -} - -.product-page__rating { - color: var(--muted-strong); - font-size: 1.1rem; -} - -.product-page__colors { - display: flex; - flex-direction: column; - gap: 1rem; -} - -.product-page__colors-label { - font-size: 0.9rem; - color: var(--muted-strong); - margin: 0; - font-weight: 600; -} - -.product-page__colors-list { - display: flex; - flex-wrap: wrap; - gap: 0.75rem; -} - -.product-page__color-option { - padding: 0.6rem 1.2rem; - border: 1px solid var(--border); - background: var(--card); - border-radius: 999px; - font-size: 0.9rem; - cursor: pointer; - transition: all 200ms ease; - color: var(--text); -} - -.product-page__color-option:hover { - border-color: var(--accent); - transform: translateY(-1px); -} - -.product-page__color-option--selected { - background: var(--accent); - color: #fff; - border-color: var(--accent); -} - -.product-page__cta { - align-self: flex-start; - border-radius: 999px; - border: none; - background: rgba(15, 23, 42, 0.85); - color: #fff; - padding: 0.75rem 2rem; - text-transform: uppercase; - letter-spacing: 0.08em; - font-size: 0.85rem; - font-weight: 600; - cursor: pointer; - transition: - transform 200ms ease, - box-shadow 200ms ease; - display: inline-flex; - align-items: center; - gap: 0.5rem; -} - -.product-page__cta:hover { - transform: translateY(-1px); - box-shadow: 0 8px 20px rgba(15, 23, 42, 0.2); -} - -.product-page__info { - margin-top: 1rem; - padding-top: 2rem; - border-top: 1px solid var(--border); -} - -.product-page__info h3 { - font-size: 1.2rem; - margin: 0 0 1rem 0; -} - -.product-page__info ul { - list-style: none; - padding: 0; - margin: 0; - display: flex; - flex-direction: column; - gap: 0.75rem; - color: var(--muted-strong); -} - -.product-page__info li { - padding-left: 1.5rem; - position: relative; -} - -.product-page__info li::before { - content: '•'; - position: absolute; - left: 0; - color: var(--accent); - font-weight: bold; -} - -.product-page__not-found { - text-align: center; - padding: 4rem 2rem; - display: flex; - flex-direction: column; - gap: 1.5rem; - align-items: center; -} - -.product-page__not-found h1 { - font-size: 2.5rem; - margin: 0; -} - -.product-page__not-found p { - color: var(--muted-strong); - font-size: 1.1rem; - margin: 0; -} - -.reviews { - margin-top: 4rem; - padding-top: 3rem; - border-top: 1px solid var(--border); -} - -.reviews__title { - font-size: 2rem; - margin: 0 0 2rem 0; -} - -.reviews__list { - display: flex; - flex-direction: column; - gap: 1.5rem; - margin-bottom: 3rem; -} - -.review { - background: var(--card); - border: 1px solid var(--border); - border-radius: 20px; - padding: 1.5rem; - display: flex; - flex-direction: column; - gap: 0.75rem; -} - -.review__header { - display: flex; - justify-content: space-between; - align-items: flex-start; - gap: 1rem; - flex-wrap: wrap; -} - -.review__header > div { - display: flex; - flex-direction: column; - gap: 0.5rem; -} - -.review__name { - font-weight: 600; - margin: 0; - color: var(--text); -} - -.review__date { - font-size: 0.85rem; - color: var(--muted-strong); -} - -.review__text { - margin: 0; - line-height: 1.6; - color: var(--muted-strong); -} - -.reviews__form { - background: var(--card); - border: 1px solid var(--border); - border-radius: 24px; - padding: 2rem; - display: flex; - flex-direction: column; - gap: 1.5rem; -} - -.reviews__form-title { - font-size: 1.3rem; - margin: 0; -} - -.reviews__form-group { - display: flex; - flex-direction: column; - gap: 0.5rem; -} - -.reviews__label { - font-size: 0.9rem; - font-weight: 600; - color: var(--text); -} - -.reviews__input, -.reviews__textarea { - padding: 0.75rem 1rem; - border: 1px solid var(--border); - border-radius: 12px; - font-size: 1rem; - font-family: inherit; - background: var(--bg); - color: var(--text); - transition: - border-color 200ms ease, - box-shadow 200ms ease; -} - -.reviews__input:focus, -.reviews__textarea:focus { - outline: none; - border-color: var(--accent); - box-shadow: 0 0 0 3px rgba(241, 84, 53, 0.1); -} - -.reviews__input:disabled, -.reviews__textarea:disabled { - opacity: 0.6; - cursor: not-allowed; -} - -.reviews__textarea { - resize: vertical; - min-height: 100px; -} - -.reviews__input--captcha { - max-width: 120px; -} - -.reviews__error { - color: var(--accent); - font-size: 0.9rem; - margin: 0; - padding: 0.75rem 1rem; - background: rgba(241, 84, 53, 0.1); - border-radius: 8px; - border: 1px solid rgba(241, 84, 53, 0.2); -} - -.reviews__submit { - align-self: flex-start; - margin-top: 0.5rem; -} - -.reviews__submit:disabled { - opacity: 0.6; - cursor: not-allowed; -} - -.star-rating { - display: inline-flex; - gap: 0.25rem; - align-items: center; -} - -.star-rating__star { - background: none; - border: none; - padding: 0; - font-size: 1.25rem; - color: rgba(15, 23, 42, 0.2); - cursor: default; - transition: - color 150ms ease, - transform 150ms ease; - line-height: 1; -} - -.star-rating__star--filled { - color: #fbbf24; -} - -.star-rating__star--interactive { - cursor: pointer; -} - -.star-rating__star--interactive:hover { - transform: scale(1.15); -} - -.star-rating__star:disabled { - cursor: default; -} - -@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; - } - - .product-page { - padding: 1rem; - } - - .product-page__content { - gap: 2rem; - } - - .product-page__meta { - font-size: 1.2rem; - } - - .products-page { - padding: 1rem; - } - - .reviews { - margin-top: 2rem; - padding-top: 2rem; - } - - .reviews__form { - padding: 1.5rem; - } - - .review__header { - flex-direction: column; - align-items: flex-start; - } -} - -.checkout-page { - max-width: 1400px; - margin: 0 auto; - padding: 1rem 2rem 2rem; -} - -.checkout-page__empty { - text-align: center; - padding: 4rem 2rem; - display: flex; - flex-direction: column; - gap: 1.5rem; - align-items: center; -} - -.checkout-page__empty h1 { - font-size: 2.5rem; - margin: 0; -} - -.checkout-page__empty p { - color: var(--muted-strong); - font-size: 1.1rem; - margin: 0; -} - -.checkout-page__content { - display: grid; - grid-template-columns: 1fr 400px; - gap: 3rem; - margin-top: 2rem; -} - -.checkout-page__title { - font-size: 2.5rem; - margin: 0 0 2rem 0; -} - -.checkout-form { - display: flex; - flex-direction: column; - gap: 2rem; -} - -.checkout-form__section { - background: var(--card); - border: 1px solid var(--border); - border-radius: 24px; - padding: 2rem; - display: flex; - flex-direction: column; - gap: 1.5rem; -} - -.checkout-form__section-title { - font-size: 1.3rem; - margin: 0 0 0.5rem 0; - font-weight: 600; -} - -.checkout-form__group { - display: flex; - flex-direction: column; - gap: 0.5rem; -} - -.checkout-form__row { - display: grid; - grid-template-columns: 2fr 1fr 1fr; - gap: 1rem; -} - -.checkout-form__label { - font-size: 0.9rem; - font-weight: 600; - color: var(--text); -} - -.checkout-form__input { - padding: 0.75rem 1rem; - border: 1px solid var(--border); - border-radius: 12px; - font-size: 1rem; - font-family: inherit; - background: var(--bg); - color: var(--text); - transition: - border-color 200ms ease, - box-shadow 200ms ease; -} - -.checkout-form__input:focus { - outline: none; - border-color: var(--accent); - box-shadow: 0 0 0 3px rgba(241, 84, 53, 0.1); -} - -.checkout-form__error { - background: rgba(241, 84, 53, 0.1); - border: 1px solid rgba(241, 84, 53, 0.2); - border-radius: 12px; - padding: 1rem; - color: var(--accent); -} - -.checkout-form__error p { - margin: 0; -} - -.checkout-form__submit { - margin-top: 1rem; -} - -.checkout-page__summary { - background: var(--card); - border: 1px solid var(--border); - border-radius: 24px; - padding: 2rem; - height: fit-content; - position: sticky; - top: 2rem; -} - -.checkout-page__summary-title { - font-size: 1.5rem; - margin: 0 0 1.5rem 0; - font-weight: 600; -} - -.checkout-page__items { - display: flex; - flex-direction: column; - gap: 1rem; - margin-bottom: 2rem; - padding-bottom: 2rem; - border-bottom: 1px solid var(--border); -} - -.checkout-page__item { - display: flex; - justify-content: space-between; - align-items: flex-start; - gap: 1rem; -} - -.checkout-page__item-info { - flex: 1; -} - -.checkout-page__item-info h3 { - font-size: 1rem; - margin: 0 0 0.25rem 0; - font-weight: 600; -} - -.checkout-page__item-info p { - font-size: 0.85rem; - color: var(--muted-strong); - margin: 0; -} - -.checkout-page__item-price { - font-weight: 600; - font-size: 1rem; -} - -.checkout-page__totals { - display: flex; - flex-direction: column; - gap: 0.75rem; -} - -.checkout-page__total-row { - display: flex; - justify-content: space-between; - color: var(--muted-strong); -} - -.checkout-page__total-row--final { - font-size: 1.2rem; - color: var(--text); - font-weight: 600; - padding-top: 0.75rem; - border-top: 1px solid var(--border); -} - -.checkout-page__success { - text-align: center; - padding: 4rem 2rem; - display: flex; - flex-direction: column; - align-items: center; - gap: 2rem; - max-width: 600px; - margin: 0 auto; -} - -.checkout-page__success-icon { - margin-bottom: 1rem; -} - -.checkout-page__success-title { - font-size: 2.5rem; - margin: 0; - font-weight: 600; -} - -.checkout-page__success-message { - font-size: 1.1rem; - color: var(--muted-strong); - margin: 0; - line-height: 1.6; -} - -.checkout-page__success-details { - background: var(--card); - border: 1px solid var(--border); - border-radius: 24px; - padding: 2rem; - width: 100%; - display: flex; - flex-direction: column; - gap: 1rem; -} - -.checkout-page__success-detail { - display: flex; - justify-content: space-between; - align-items: center; - padding-bottom: 1rem; - border-bottom: 1px solid var(--border); -} - -.checkout-page__success-detail:last-child { - border-bottom: none; - padding-bottom: 0; -} - -.checkout-page__success-label { - font-weight: 600; - color: var(--muted-strong); -} - -.checkout-page__success-value { - font-weight: 600; - color: var(--text); -} - -.checkout-page__success-actions { - margin-top: 1rem; -} - -@media (max-width: 1024px) { - .checkout-page__content { - grid-template-columns: 1fr; - } - - .checkout-page__summary { - position: static; - } - - .checkout-form__row { - grid-template-columns: 1fr; - } -} diff --git a/src/App.tsx b/src/App.tsx index ce5992b..e091ed2 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,6 @@ import { useEffect, useMemo, useRef, useState } from 'react' import { Link, Route, Routes } from 'react-router-dom' import { useQuery } from '@apollo/client/react' -import './App.css' import { type Product } from './data/products' import ProductPage from './ProductPage' import ProductsPage from './ProductsPage' @@ -49,26 +48,35 @@ function CartLine({ onRemove: () => void }) { return ( -
  • -
    -

    {item.product.name}

    +
  • +
    +

    {item.product.name}

    {currency.format(item.product.price)}
    -
    -
    -
    +
    +
    +
    - + {item.quantity}
    - - {currency.format(item.product.price * item.quantity)} - + {currency.format(item.product.price * item.quantity)}
  • ) @@ -134,56 +141,45 @@ function Layout({ const { isAuthenticated, user, logout } = useAuth() return ( -
    -
    +
    +
    -

    Your bag

    -

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

    +

    Your bag

    +

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

    -
    +
    {isAuthenticated ? ( <> - + {user?.email} ) : ( - + Login )} @@ -191,21 +187,30 @@ function Layout({
    {isCartOpen && ( -
    -
    +
    +
    -

    Shopping bag

    -

    Ready to ship

    +

    Shopping bag

    +

    Ready to ship

    -
    -

    {freeShippingMessage}

    +

    {freeShippingMessage}

    {cartItems.length === 0 ? ( -

    Your basket is empty – add your favorite finds.

    +

    + Your basket is empty – add your favorite finds. +

    ) : ( -
      +
        {cartItems.map((item) => ( )} -
        -
        +
        +
        Subtotal {currency.format(subtotal)}
        -
        +
        Shipping {shipping === 0 ? 'Complimentary' : currency.format(shipping)}
        -
        +
        Total {currency.format(total)}
        Proceed to checkout @@ -269,20 +273,23 @@ function HomePage() { return ( <> -
        -
        -

        New season edit

        -

        Meet the modern home shop

        -

        +

        +
        +

        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} +
        + {heroProduct.name}
        -
        +
        {perks.map((perk) => ( -
        -

        {perk.title}

        -

        {perk.detail}

        +
        +

        {perk.title}

        +

        {perk.detail}

        ))}
        -
        -
        -

        Shop by room

        -

        Spaces with intention

        -

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

        +
        +
        +

        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

        +
        +

        {category.category}

        +

        {category.count} curated pieces

        ))}
        -
        -
        - {editorialHighlight.name} +
        +
        + {editorialHighlight.name}
        -
        -

        From the studio

        -

        Layered neutrals, elevated silhouettes

        -

        +

        +

        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/CheckoutPage.tsx b/src/CheckoutPage.tsx index bc86a49..a67e1ad 100644 --- a/src/CheckoutPage.tsx +++ b/src/CheckoutPage.tsx @@ -1,15 +1,14 @@ import { useState } from 'react' import type { FormEvent } from 'react' -import { useNavigate, Link } from 'react-router-dom' import { useMutation } from '@apollo/client/react' -import './App.css' import { CREATE_CHECKOUT } from './graphql/queries' import type { Product } from './data/products' - -const currency = new Intl.NumberFormat('en-US', { - style: 'currency', - currency: 'USD', -}) +import ErrorMessage from './components/ErrorMessage' +import PageContainer from './components/PageContainer' +import EmptyCartState from './components/checkout/EmptyCartState' +import ShippingAddressForm from './components/checkout/ShippingAddressForm' +import PaymentMethodSelect from './components/checkout/PaymentMethodSelect' +import OrderSummary from './components/checkout/OrderSummary' type CartLineItem = { product: Product @@ -55,7 +54,6 @@ type CreateCheckoutMutationResult = { } export default function CheckoutPage({ cartItems, subtotal, shipping, total }: CheckoutPageProps) { - const navigate = useNavigate() const [createCheckout, { loading, error }] = useMutation(CREATE_CHECKOUT) const [formData, setFormData] = useState({ @@ -112,14 +110,16 @@ export default function CheckoutPage({ cartItems, subtotal, shipping, total }: C if (data?.createCheckout) { // Navigate to checkout success page or show confirmation - navigate(`/checkout/${data.createCheckout.id}/success`) + window.location.href = `/checkout/${data.createCheckout.id}/success` } } catch (err) { console.error('Checkout error:', err) } } - const handleChange = (e: React.ChangeEvent) => { + const handleChange = ( + e: React.ChangeEvent, + ) => { setFormData({ ...formData, [e.target.name]: e.target.value, @@ -127,219 +127,33 @@ export default function CheckoutPage({ cartItems, subtotal, shipping, total }: C } if (cartItems.length === 0) { - return ( -
        -
        -
        -

        Your cart is empty

        -

        Add items to your cart before proceeding to checkout.

        - -
        -
        -
        - ) + return } return ( -
        -
        - - ← Back to products - - -
        -
        -

        Checkout

        - -
        -
        -

        Shipping Address

        -
        -
        - - -
        - -
        - - -
        -
        - -
        - - -
        + +
        +
        +

        Checkout

        -
        -
        - - -
        + + + -
        - - -
        + {error && } -
        - - -
        -
        - -
        - - -
        -
        - -
        -

        Payment Method

        -
        - - -
        -
        - - {error && ( -
        -

        Error processing checkout: {error.message}

        -
        - )} - - -
        -
        - -
        -

        Order Summary

        -
        - {cartItems.map((item) => ( -
        -
        -

        {item.product.name}

        -

        Quantity: {item.quantity}

        -
        -
        - {currency.format(item.product.price * item.quantity)} -
        -
        - ))} -
        -
        -
        - Subtotal - {currency.format(subtotal)} -
        -
        - Shipping - {shipping === 0 ? 'Complimentary' : currency.format(shipping)} -
        -
        - Total - {currency.format(total)} -
        -
        -
        + +
        + +
        -
        + ) } diff --git a/src/CheckoutSuccessPage.tsx b/src/CheckoutSuccessPage.tsx index fe66870..759fd95 100644 --- a/src/CheckoutSuccessPage.tsx +++ b/src/CheckoutSuccessPage.tsx @@ -1,8 +1,10 @@ import { useEffect } from 'react' import { useParams } from 'react-router-dom' import { useQuery } from '@apollo/client/react' -import './App.css' import { GET_CHECKOUT } from './graphql/queries' +import LoadingState from './components/LoadingState' +import EmptyState from './components/EmptyState' +import PageContainer from './components/PageContainer' const currency = new Intl.NumberFormat('en-US', { style: 'currency', @@ -57,98 +59,76 @@ export default function CheckoutSuccessPage({ onClearCart }: CheckoutSuccessPage }, []) if (loading) { - return ( -
        -
        -
        -

        Loading...

        -
        -
        -
        - ) + return } const checkout = data?.checkout if (!checkout) { return ( -
        -
        -
        -

        Checkout not found

        -

        Sorry, we couldn't find your checkout information.

        - -
        -
        -
        + { + window.location.href = '/products' + }} + /> ) } return ( -
        -
        -
        -
        - - - - + +
        +
        + + + + +
        +

        Order Confirmed!

        +

        + Thank you for your order. We've received your payment and will begin processing your + shipment shortly. +

        +
        +
        + Order ID: + {checkout.id}
        -

        Order Confirmed!

        -

        - Thank you for your order. We've received your payment and will begin processing your - shipment shortly. -

        -
        -
        - Order ID: - {checkout.id} -
        -
        - Total: - - {currency.format(checkout.total)} - -
        -
        - Status: - {checkout.status} -
        +
        + Total: + {currency.format(checkout.total)}
        -
        - +
        + Status: + {checkout.status}
        +
        + +
        -
        + ) } diff --git a/src/LoginPage.tsx b/src/LoginPage.tsx index d6c96da..142bbd3 100644 --- a/src/LoginPage.tsx +++ b/src/LoginPage.tsx @@ -1,7 +1,9 @@ import { useState, type FormEvent } from 'react' import { useNavigate, Link, useLocation } from 'react-router-dom' import { useAuth } from './contexts/AuthContext' -import './App.css' +import FormField from './components/FormField' +import ErrorMessage from './components/ErrorMessage' +import PageContainer from './components/PageContainer' export default function LoginPage() { const navigate = useNavigate() @@ -30,72 +32,51 @@ export default function LoginPage() { } return ( -
        -
        -
        -
        -

        Login

        + +
        +

        Login

        -
        - {error && ( -
        -

        {error}

        -
        - )} + + {error && } -
        - - setEmail(e.target.value)} - required - autoComplete="email" - /> -
        + setEmail(e.target.value)} + required + autoComplete="email" + /> -
        - - setPassword(e.target.value)} - required - autoComplete="current-password" - /> -
        + setPassword(e.target.value)} + required + autoComplete="current-password" + /> - + -

        - Don't have an account?{' '} - - Register here - -

        - -
        -
        +

        + Don't have an account?{' '} + + Register here + +

        +
        -
        + ) } diff --git a/src/ProductCard.tsx b/src/ProductCard.tsx index 37b0eec..647de8a 100644 --- a/src/ProductCard.tsx +++ b/src/ProductCard.tsx @@ -1,13 +1,10 @@ import { useEffect, useState } from 'react' import { Link } from 'react-router-dom' -import './App.css' import { type Product } from './data/products' import { getAverageRating } from './utils/reviews' - -const currency = new Intl.NumberFormat('en-US', { - style: 'currency', - currency: 'USD', -}) +import ProductImage from './components/product/ProductImage' +import ProductMeta from './components/product/ProductMeta' +import AddToCartButton from './components/product/AddToCartButton' type ProductCardProps = { product: Product @@ -36,63 +33,46 @@ export default function ProductCard({ product, onAdd, isHighlighted }: ProductCa } }, [product.id]) - const handleAddClick = (e: React.MouseEvent) => { - e.preventDefault() - e.stopPropagation() - onAdd() - } - return ( -
        - {product.name} - {product.badge && {product.badge}} -
        -
        -

        {product.category}

        -

        {product.name}

        -

        {product.description}

        -
        - {currency.format(product.price)} - {averageRating !== null && ( - ★ {averageRating.toFixed(1)} - )} -
        -
        + +
        +

        {product.category}

        +

        {product.name}

        +

        {product.description}

        + +
        {product.colors.map((color) => ( {color} ))}
        - + onAdd()} + isHighlighted={isHighlighted} + className="px-5 py-2 text-xs" + /> +
        ) diff --git a/src/ProductPage.tsx b/src/ProductPage.tsx index 99bb768..d5924ae 100644 --- a/src/ProductPage.tsx +++ b/src/ProductPage.tsx @@ -1,16 +1,15 @@ import { useState, useEffect } from 'react' -import { Link, useParams } from 'react-router-dom' +import { useParams } from 'react-router-dom' import { useQuery } from '@apollo/client/react' -import './App.css' import Reviews from './Reviews' import { getAverageRating } from './utils/reviews' import { GET_PRODUCT } from './graphql/queries' import type { Product } from './data/products' - -const currency = new Intl.NumberFormat('en-US', { - style: 'currency', - currency: 'USD', -}) +import LoadingState from './components/LoadingState' +import EmptyState from './components/EmptyState' +import PageContainer from './components/PageContainer' +import ProductImage from './components/product/ProductImage' +import ProductDetails from './components/product/ProductDetails' type ProductPageProps = { onAddToCart: (productId: string) => void @@ -67,30 +66,17 @@ export default function ProductPage({ onAddToCart, isHighlighted }: ProductPageP }, [product]) if (loading) { - return ( -
        -
        -
        -

        Loading...

        -
        -
        -
        - ) + return } if (error || !product) { return ( -
        -
        -
        -

        Product not found

        -

        Sorry, we couldn't find the product you're looking for.

        - - Back to products - -
        -
        -
        + ) } @@ -103,90 +89,20 @@ export default function ProductPage({ onAddToCart, isHighlighted }: ProductPageP } return ( -
        -
        - - ← Back to products - - -
        -
        - {product.name} - {product.badge && {product.badge}} -
        - -
        -

        {product.category}

        -

        {product.name}

        -

        {product.description}

        - -
        - {currency.format(product.price)} - {averageRating !== null && ( - ★ {averageRating.toFixed(1)} - )} -
        - -
        -

        Available colors:

        -
        - {product.colors.map((color: string) => ( - - ))} -
        -
        - - - -
        -

        Product details

        -
          -
        • Premium materials and craftsmanship
        • -
        • 30-day return policy
        • -
        • Complimentary shipping on orders over $150
        • -
        • Design consultation available
        • -
        -
        -
        -
        - - + +
        + +
        -
        + + + ) } diff --git a/src/ProductsPage.tsx b/src/ProductsPage.tsx index eb0f97b..820cdb1 100644 --- a/src/ProductsPage.tsx +++ b/src/ProductsPage.tsx @@ -1,10 +1,12 @@ import { useEffect } from 'react' -import { Link, useLocation } from 'react-router-dom' +import { useLocation } from 'react-router-dom' import { useQuery } from '@apollo/client/react' -import './App.css' import ProductCard from './ProductCard' import { GET_PRODUCTS } from './graphql/queries' import type { Product } from './data/products' +import LoadingState from './components/LoadingState' +import ErrorMessage from './components/ErrorMessage' +import PageContainer from './components/PageContainer' type ProductsPageProps = { addToCart: (productId: string) => void @@ -24,25 +26,22 @@ export default function ProductsPage({ addToCart, isHighlighted }: ProductsPageP window.scrollTo({ top: 0, behavior: 'smooth' }) }, [location.pathname]) - if (loading) return
        Loading products...
        - if (error) return
        Error loading products: {error.message}
        + if (loading) return + if (error) return const products = data?.products || [] return ( -
        - - ← Back to home - -
        -
        -

        Featured pieces

        -

        Crafted to layer beautifully

        -

        + +

        +
        +

        Featured pieces

        +

        Crafted to layer beautifully

        +

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

        -
        +
        {products.map((product: Product) => (
        -
        + ) } diff --git a/src/RegisterPage.tsx b/src/RegisterPage.tsx index f5b6679..62d83a5 100644 --- a/src/RegisterPage.tsx +++ b/src/RegisterPage.tsx @@ -1,7 +1,9 @@ import { useState, type FormEvent } from 'react' import { useNavigate, Link } from 'react-router-dom' import { useAuth } from './contexts/AuthContext' -import './App.css' +import FormField from './components/FormField' +import ErrorMessage from './components/ErrorMessage' +import PageContainer from './components/PageContainer' export default function RegisterPage() { const navigate = useNavigate() @@ -35,108 +37,77 @@ export default function RegisterPage() { } return ( -
        -
        -
        -
        -

        Register

        + +
        +

        Register

        -
        - {error && ( -
        -

        {error}

        -
        - )} + + {error && } -
        -
        - - setFirstName(e.target.value)} - required - autoComplete="given-name" - /> -
        +
        + setFirstName(e.target.value)} + required + autoComplete="given-name" + /> -
        - - setLastName(e.target.value)} - required - autoComplete="family-name" - /> -
        -
        + setLastName(e.target.value)} + required + autoComplete="family-name" + /> +
        -
        - - setEmail(e.target.value)} - required - autoComplete="email" - /> -
        + setEmail(e.target.value)} + required + autoComplete="email" + /> -
        - - setPassword(e.target.value)} - required - autoComplete="new-password" - minLength={6} - /> - - Must be at least 6 characters - -
        + setPassword(e.target.value)} + required + autoComplete="new-password" + minLength={6} + small="Must be at least 6 characters" + /> - + -

        - Already have an account?{' '} - - Login here - -

        - -
        -
        +

        + Already have an account?{' '} + + Login here + +

        +
        -
        + ) } - diff --git a/src/Reviews.tsx b/src/Reviews.tsx index 85f8cf4..4b34f58 100644 --- a/src/Reviews.tsx +++ b/src/Reviews.tsx @@ -1,5 +1,6 @@ import { useState, useEffect } from 'react' -import './App.css' +import ReviewItem from './components/reviews/ReviewItem' +import ReviewForm from './components/reviews/ReviewForm' type Review = { id: string @@ -10,76 +11,12 @@ type Review = { date: string } -type StarRatingProps = { - rating: number - onRatingChange?: (rating: number) => void - interactive?: boolean -} - -function StarRating({ rating, onRatingChange, interactive = false }: StarRatingProps) { - const [hoverRating, setHoverRating] = useState(0) - - const handleClick = (value: number) => { - if (interactive && onRatingChange) { - onRatingChange(value) - } - } - - const handleMouseEnter = (value: number) => { - if (interactive) { - setHoverRating(value) - } - } - - const handleMouseLeave = () => { - if (interactive) { - setHoverRating(0) - } - } - - const displayRating = hoverRating || rating - - return ( -
        - {[1, 2, 3, 4, 5].map((value) => ( - - ))} -
        - ) -} - type ReviewsProps = { productId: string } -function generateCaptcha() { - const num1 = Math.floor(Math.random() * 10) + 1 - const num2 = Math.floor(Math.random() * 10) + 1 - return { question: `${num1} + ${num2}`, answer: num1 + num2 } -} - export default function Reviews({ productId }: ReviewsProps) { const [reviews, setReviews] = useState([]) - const [name, setName] = useState('') - const [text, setText] = useState('') - const [rating, setRating] = useState(0) - const [captcha, setCaptcha] = useState(generateCaptcha()) - const [captchaAnswer, setCaptchaAnswer] = useState('') const [error, setError] = useState('') const [isSubmitting, setIsSubmitting] = useState(false) @@ -104,155 +41,53 @@ export default function Reviews({ productId }: ReviewsProps) { loadReviews() }, [productId]) - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault() - setError('') - - if (!name.trim()) { - setError('Please enter your name') - return - } - - if (!text.trim()) { - setError('Please enter your review') - return - } - - if (rating === 0) { - setError('Please select a rating') - return - } - - const answer = parseInt(captchaAnswer, 10) - if (isNaN(answer) || answer !== captcha.answer) { - setError('Incorrect captcha answer. Please try again.') - setCaptcha(generateCaptcha()) - setCaptchaAnswer('') - return - } - + const handleSubmit = async (review: { name: string; text: string; rating: number }) => { setIsSubmitting(true) + setError('') // Simulate a brief delay for better UX - setTimeout(() => { - const newReview: Review = { - id: Date.now().toString(), - productId, - name: name.trim(), - text: text.trim(), - rating, - date: new Date().toISOString(), - } - - const updatedReviews = [newReview, ...reviews] - setReviews(updatedReviews) - localStorage.setItem(`reviews-${productId}`, JSON.stringify(updatedReviews)) + return new Promise((resolve) => { + setTimeout(() => { + const newReview: Review = { + id: Date.now().toString(), + productId, + name: review.name, + text: review.text, + rating: review.rating, + date: new Date().toISOString(), + } - // Dispatch custom event to notify other components - window.dispatchEvent(new CustomEvent('reviewAdded', { detail: { productId } })) + const updatedReviews = [newReview, ...reviews] + setReviews(updatedReviews) + localStorage.setItem(`reviews-${productId}`, JSON.stringify(updatedReviews)) - // Reset form - setName('') - setText('') - setRating(0) - setCaptchaAnswer('') - setCaptcha(generateCaptcha()) - setIsSubmitting(false) - setError('') - }, 300) - } + // Dispatch custom event to notify other components + window.dispatchEvent(new CustomEvent('reviewAdded', { detail: { productId } })) - const formatDate = (dateString: string) => { - const date = new Date(dateString) - return date.toLocaleDateString('en-US', { - year: 'numeric', - month: 'long', - day: 'numeric', + setIsSubmitting(false) + resolve() + }, 300) }) } return ( -
        -

        Customer Reviews

        +
        +

        Customer Reviews

        {reviews.length > 0 && ( -
        +
        {reviews.map((review) => ( -
        -
        -
        -

        {review.name}

        - -
        - -
        -

        {review.text}

        -
        + ))}
        )} -
        -

        Write a review

        - -
        - - setName(e.target.value)} - placeholder="Enter your name" - disabled={isSubmitting} - /> -
        - -
        - - -
        - -
        - -