3 Commits

Author SHA1 Message Date
Unchained
a47698d5ca fix(saleor): Fix remaining WooCommerce references and configuration
- Fix syntax error in Checkout.ts (extra semicolon)
- Update NewHero.tsx to use Saleor types and store
- Update page.tsx to use Saleor getProducts
- Add Saleor API domain to next.config.ts images config
2026-03-21 13:00:16 +02:00
Unchained
1b733c63d5 feat(saleor): Phase 5 - Remove WooCommerce
- Remove @woocommerce/woocommerce-rest-api dependency
- Delete src/lib/woocommerce.ts
- Delete src/stores/cartStore.ts (replaced by saleorCheckoutStore)
- Clean up package.json dependencies
- Project now fully migrated to Saleor GraphQL API
2026-03-21 12:45:56 +02:00
Unchained
d43481716d feat(saleor): Phase 4 - Checkout Flow
- Create checkout page with form validation
- Implement shipping/billing address forms
- Add Cash on Delivery (COD) payment method
- Integrate Saleor checkout completion mutation
- Add order success page with confirmation
- Handle checkout errors gracefully
- Display order summary with line items
2026-03-21 12:45:09 +02:00
9 changed files with 523 additions and 589 deletions

View File

@@ -17,6 +17,16 @@ const nextConfig: NextConfig = {
hostname: "minio-api.nodecrew.me",
pathname: "/**",
},
{
protocol: "https",
hostname: "api.manoonoils.com",
pathname: "/**",
},
{
protocol: "https",
hostname: "**.saleor.cloud",
pathname: "/**",
},
],
},
};

372
package-lock.json generated
View File

@@ -9,7 +9,6 @@
"version": "0.1.0",
"dependencies": {
"@apollo/client": "^4.1.6",
"@woocommerce/woocommerce-rest-api": "^1.0.2",
"clsx": "^2.1.1",
"framer-motion": "^12.34.4",
"graphql": "^16.13.1",
@@ -2739,21 +2738,6 @@
"win32"
]
},
"node_modules/@woocommerce/woocommerce-rest-api": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@woocommerce/woocommerce-rest-api/-/woocommerce-rest-api-1.0.2.tgz",
"integrity": "sha512-G+0VwM0MINF83KnT7Rg/htm9EEYADWvDPT/UWEJdZ0de1vXvsPrr4M1ksKaxgKHO8qIJViRrIHCtrui2JoVA+Q==",
"license": "MIT",
"dependencies": {
"axios": "^1.6.8",
"create-hmac": "^1.1.7",
"oauth-1.0a": "^2.2.6",
"url-parse": "^1.4.7"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/@wry/caches": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@wry/caches/-/caches-1.0.1.tgz",
@@ -3052,16 +3036,11 @@
"node": ">= 0.4"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/available-typed-arrays": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
"integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"possible-typed-array-names": "^1.0.0"
@@ -3083,17 +3062,6 @@
"node": ">=4"
}
},
"node_modules/axios": {
"version": "1.13.6",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz",
"integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.11",
"form-data": "^4.0.5",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/axobject-query": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
@@ -3185,6 +3153,7 @@
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
"integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.0",
@@ -3203,6 +3172,7 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
@@ -3216,6 +3186,7 @@
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
@@ -3275,20 +3246,6 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/cipher-base": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.7.tgz",
"integrity": "sha512-Mz9QMT5fJe7bKI7MH31UilT5cEK5EHHRCccw/YRFsRY47AuNgaV6HY3rscp0/I4Q+tTW/5zoqpSeRRI54TkDWA==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.4",
"safe-buffer": "^5.2.1",
"to-buffer": "^1.2.2"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/client-only": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
@@ -3324,18 +3281,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -3350,39 +3295,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"license": "MIT"
},
"node_modules/create-hash": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
"integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==",
"license": "MIT",
"dependencies": {
"cipher-base": "^1.0.1",
"inherits": "^2.0.1",
"md5.js": "^1.3.4",
"ripemd160": "^2.0.1",
"sha.js": "^2.4.0"
}
},
"node_modules/create-hmac": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz",
"integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==",
"license": "MIT",
"dependencies": {
"cipher-base": "^1.0.3",
"create-hash": "^1.1.0",
"inherits": "^2.0.1",
"ripemd160": "^2.0.0",
"safe-buffer": "^5.0.1",
"sha.js": "^2.4.8"
}
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -3501,6 +3413,7 @@
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-define-property": "^1.0.0",
@@ -3532,15 +3445,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
@@ -3567,6 +3471,7 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
@@ -3678,6 +3583,7 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -3687,6 +3593,7 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -3724,6 +3631,7 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
@@ -3736,6 +3644,7 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
@@ -4356,30 +4265,11 @@
"dev": true,
"license": "ISC"
},
"node_modules/follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/for-each": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
"integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-callable": "^1.2.7"
@@ -4391,22 +4281,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/form-data": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/framer-motion": {
"version": "12.34.4",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.34.4.tgz",
@@ -4438,6 +4312,7 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -4498,6 +4373,7 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
@@ -4522,6 +4398,7 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"dev": true,
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
@@ -4609,6 +4486,7 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -4675,6 +4553,7 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-define-property": "^1.0.0"
@@ -4703,6 +4582,7 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -4715,6 +4595,7 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
@@ -4726,25 +4607,11 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hash-base": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.2.tgz",
"integrity": "sha512-Bb33KbowVTIj5s7Ked1OsqHUeCpz//tPwR+E2zJgJKo9Z5XolZ9b6bdUgjmYlwnWhoOQKoTd1TYToZGn5mAYOg==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.4",
"readable-stream": "^2.3.8",
"safe-buffer": "^5.2.1",
"to-buffer": "^1.2.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
@@ -4822,12 +4689,6 @@
"node": ">=0.8.19"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
"node_modules/internal-slot": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
@@ -4953,6 +4814,7 @@
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
"integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -5209,6 +5071,7 @@
"version": "1.1.15",
"resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz",
"integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"which-typed-array": "^1.1.16"
@@ -5270,6 +5133,7 @@
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
"integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
"dev": true,
"license": "MIT"
},
"node_modules/isexe": {
@@ -5764,22 +5628,12 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/md5.js": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
"integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==",
"license": "MIT",
"dependencies": {
"hash-base": "^3.0.0",
"inherits": "^2.0.1",
"safe-buffer": "^5.1.2"
}
},
"node_modules/merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@@ -5804,27 +5658,6 @@
"node": ">=8.6"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/minimatch": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
@@ -6109,12 +5942,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/oauth-1.0a": {
"version": "2.2.6",
"resolved": "https://registry.npmjs.org/oauth-1.0a/-/oauth-1.0a-2.2.6.tgz",
"integrity": "sha512-6bkxv3N4Gu5lty4viIcIAnq5GbxECviMBeKR3WX/q87SPQ8E8aursPZUtsXDnxCs787af09WPRBLqYrf/lwoYQ==",
"license": "MIT"
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -6387,6 +6214,7 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
"integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -6431,12 +6259,6 @@
"node": ">= 0.8.0"
}
},
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"license": "MIT"
},
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
@@ -6449,12 +6271,6 @@
"react-is": "^16.13.1"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -6465,12 +6281,6 @@
"node": ">=6"
}
},
"node_modules/querystringify": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
"license": "MIT"
},
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -6520,33 +6330,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"license": "MIT",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/readable-stream/node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
"license": "MIT"
},
"node_modules/readable-stream/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
"node_modules/reflect.getprototypeof": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
@@ -6591,12 +6374,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/requires-port": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
"license": "MIT"
},
"node_modules/resolve": {
"version": "1.22.11",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
@@ -6649,19 +6426,6 @@
"node": ">=0.10.0"
}
},
"node_modules/ripemd160": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.3.tgz",
"integrity": "sha512-5Di9UC0+8h1L6ZD2d7awM7E/T4uA1fJRlx6zk/NvdCCVEoAnFqvHmCuNeIKoCeIixBX/q8uM+6ycDvF8woqosA==",
"license": "MIT",
"dependencies": {
"hash-base": "^3.1.2",
"inherits": "^2.0.4"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/run-parallel": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
@@ -6716,26 +6480,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/safe-push-apply": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz",
@@ -6791,6 +6535,7 @@
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
"integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
"dev": true,
"license": "MIT",
"dependencies": {
"define-data-property": "^1.1.4",
@@ -6835,26 +6580,6 @@
"node": ">= 0.4"
}
},
"node_modules/sha.js": {
"version": "2.4.12",
"resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz",
"integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==",
"license": "(MIT AND BSD-3-Clause)",
"dependencies": {
"inherits": "^2.0.4",
"safe-buffer": "^5.2.1",
"to-buffer": "^1.2.0"
},
"bin": {
"sha.js": "bin.js"
},
"engines": {
"node": ">= 0.10"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/sharp": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
@@ -7042,21 +6767,6 @@
"node": ">= 0.4"
}
},
"node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/string_decoder/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
"node_modules/string.prototype.includes": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz",
@@ -7321,20 +7031,6 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/to-buffer": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz",
"integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==",
"license": "MIT",
"dependencies": {
"isarray": "^2.0.5",
"safe-buffer": "^5.2.1",
"typed-array-buffer": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -7410,6 +7106,7 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz",
"integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.3",
@@ -7623,16 +7320,6 @@
"punycode": "^2.1.0"
}
},
"node_modules/url-parse": {
"version": "1.5.10",
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
"integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
"license": "MIT",
"dependencies": {
"querystringify": "^2.1.1",
"requires-port": "^1.0.0"
}
},
"node_modules/use-intl": {
"version": "4.8.3",
"resolved": "https://registry.npmjs.org/use-intl/-/use-intl-4.8.3.tgz",
@@ -7654,12 +7341,6 @@
"react": "^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -7747,6 +7428,7 @@
"version": "1.1.20",
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz",
"integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==",
"dev": true,
"license": "MIT",
"dependencies": {
"available-typed-arrays": "^1.0.7",

View File

@@ -10,7 +10,6 @@
},
"dependencies": {
"@apollo/client": "^4.1.6",
"@woocommerce/woocommerce-rest-api": "^1.0.2",
"clsx": "^2.1.1",
"framer-motion": "^12.34.4",
"graphql": "^16.13.1",

464
src/app/checkout/page.tsx Normal file
View File

@@ -0,0 +1,464 @@
"use client";
import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import Link from "next/link";
import Image from "next/image";
import Header from "@/components/layout/Header";
import Footer from "@/components/layout/Footer";
import { useSaleorCheckoutStore } from "@/stores/saleorCheckoutStore";
import { formatPrice } from "@/lib/saleor";
import { saleorClient } from "@/lib/saleor/client";
import {
CHECKOUT_SHIPPING_ADDRESS_UPDATE,
CHECKOUT_BILLING_ADDRESS_UPDATE,
CHECKOUT_COMPLETE,
} from "@/lib/saleor/mutations/Checkout";
interface AddressForm {
firstName: string;
lastName: string;
streetAddress1: string;
streetAddress2: string;
city: string;
postalCode: string;
phone: string;
}
export default function CheckoutPage() {
const router = useRouter();
const { checkout, refreshCheckout, getLines, getTotal } = useSaleorCheckoutStore();
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [orderComplete, setOrderComplete] = useState(false);
const [orderNumber, setOrderNumber] = useState<string | null>(null);
const [sameAsShipping, setSameAsShipping] = useState(true);
const [shippingAddress, setShippingAddress] = useState<AddressForm>({
firstName: "",
lastName: "",
streetAddress1: "",
streetAddress2: "",
city: "",
postalCode: "",
phone: "",
});
const [billingAddress, setBillingAddress] = useState<AddressForm>({
firstName: "",
lastName: "",
streetAddress1: "",
streetAddress2: "",
city: "",
postalCode: "",
phone: "",
});
const lines = getLines();
const total = getTotal();
useEffect(() => {
if (!checkout) {
refreshCheckout();
}
}, [checkout, refreshCheckout]);
// Redirect if cart is empty
useEffect(() => {
if (lines.length === 0 && !orderComplete) {
// Optionally redirect to cart or products
// router.push("/products");
}
}, [lines, orderComplete, router]);
const handleShippingChange = (field: keyof AddressForm, value: string) => {
setShippingAddress((prev) => ({ ...prev, [field]: value }));
if (sameAsShipping) {
setBillingAddress((prev) => ({ ...prev, [field]: value }));
}
};
const handleBillingChange = (field: keyof AddressForm, value: string) => {
setBillingAddress((prev) => ({ ...prev, [field]: value }));
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!checkout) {
setError("No active checkout. Please try again.");
return;
}
setIsLoading(true);
setError(null);
try {
// Update shipping address
const shippingResult = await saleorClient.mutate({
mutation: CHECKOUT_SHIPPING_ADDRESS_UPDATE,
variables: {
checkoutId: checkout.id,
shippingAddress: {
...shippingAddress,
country: "RS", // Serbia
},
},
});
if (shippingResult.data?.checkoutShippingAddressUpdate?.errors?.length > 0) {
throw new Error(shippingResult.data.checkoutShippingAddressUpdate.errors[0].message);
}
// Update billing address
const billingResult = await saleorClient.mutate({
mutation: CHECKOUT_BILLING_ADDRESS_UPDATE,
variables: {
checkoutId: checkout.id,
billingAddress: {
...billingAddress,
country: "RS",
},
},
});
if (billingResult.data?.checkoutBillingAddressUpdate?.errors?.length > 0) {
throw new Error(billingResult.data.checkoutBillingAddressUpdate.errors[0].message);
}
// Complete checkout (creates order)
const completeResult = await saleorClient.mutate({
mutation: CHECKOUT_COMPLETE,
variables: {
checkoutId: checkout.id,
},
});
if (completeResult.data?.checkoutComplete?.errors?.length > 0) {
throw new Error(completeResult.data.checkoutComplete.errors[0].message);
}
const order = completeResult.data?.checkoutComplete?.order;
if (order) {
setOrderNumber(order.number);
setOrderComplete(true);
} else {
throw new Error("Failed to create order");
}
} catch (err: any) {
setError(err.message || "An error occurred during checkout");
} finally {
setIsLoading(false);
}
};
// Order Success Page
if (orderComplete) {
return (
<main className="min-h-screen">
<Header />
<section className="pt-24 pb-20 px-4">
<div className="max-w-2xl mx-auto text-center">
<div className="mb-6">
<div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
<svg className="w-8 h-8 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</div>
<h1 className="text-3xl font-serif mb-2">Order Confirmed!</h1>
<p className="text-foreground-muted">Thank you for your purchase.</p>
</div>
{orderNumber && (
<div className="bg-background-ice p-6 rounded-lg mb-6">
<p className="text-sm text-foreground-muted mb-1">Order Number</p>
<p className="text-2xl font-serif">#{orderNumber}</p>
</div>
)}
<p className="text-foreground-muted mb-8">
You will receive a confirmation email shortly. We will contact you to arrange Cash on Delivery.
</p>
<Link
href="/products"
className="inline-block px-8 py-3 bg-foreground text-white hover:bg-accent-dark transition-colors"
>
Continue Shopping
</Link>
</div>
</section>
<Footer />
</main>
);
}
return (
<main className="min-h-screen">
<Header />
<section className="pt-24 pb-20 px-4">
<div className="max-w-7xl mx-auto">
<h1 className="text-3xl font-serif mb-8">Checkout</h1>
{error && (
<div className="bg-red-50 border border-red-200 text-red-600 p-4 mb-6 rounded">
{error}
</div>
)}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
{/* Checkout Form */}
<div>
<form onSubmit={handleSubmit} className="space-y-6">
{/* Shipping Address */}
<div className="border-b border-border pb-6">
<h2 className="text-xl font-serif mb-4">Shipping Address</h2>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-1">First Name</label>
<input
type="text"
required
value={shippingAddress.firstName}
onChange={(e) => handleShippingChange("firstName", e.target.value)}
className="w-full border border-border px-4 py-2 rounded"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Last Name</label>
<input
type="text"
required
value={shippingAddress.lastName}
onChange={(e) => handleShippingChange("lastName", e.target.value)}
className="w-full border border-border px-4 py-2 rounded"
/>
</div>
<div className="col-span-2">
<label className="block text-sm font-medium mb-1">Street Address</label>
<input
type="text"
required
value={shippingAddress.streetAddress1}
onChange={(e) => handleShippingChange("streetAddress1", e.target.value)}
className="w-full border border-border px-4 py-2 rounded"
/>
</div>
<div className="col-span-2">
<input
type="text"
value={shippingAddress.streetAddress2}
onChange={(e) => handleShippingChange("streetAddress2", e.target.value)}
placeholder="Apartment, suite, etc. (optional)"
className="w-full border border-border px-4 py-2 rounded"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">City</label>
<input
type="text"
required
value={shippingAddress.city}
onChange={(e) => handleShippingChange("city", e.target.value)}
className="w-full border border-border px-4 py-2 rounded"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Postal Code</label>
<input
type="text"
required
value={shippingAddress.postalCode}
onChange={(e) => handleShippingChange("postalCode", e.target.value)}
className="w-full border border-border px-4 py-2 rounded"
/>
</div>
<div className="col-span-2">
<label className="block text-sm font-medium mb-1">Phone</label>
<input
type="tel"
required
value={shippingAddress.phone}
onChange={(e) => handleShippingChange("phone", e.target.value)}
className="w-full border border-border px-4 py-2 rounded"
/>
</div>
</div>
</div>
{/* Billing Address Toggle */}
<div className="border-b border-border pb-6">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={sameAsShipping}
onChange={(e) => setSameAsShipping(e.target.checked)}
className="w-4 h-4"
/>
<span>Billing address same as shipping</span>
</label>
</div>
{/* Billing Address (if different) */}
{!sameAsShipping && (
<div className="border-b border-border pb-6">
<h2 className="text-xl font-serif mb-4">Billing Address</h2>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-1">First Name</label>
<input
type="text"
required
value={billingAddress.firstName}
onChange={(e) => handleBillingChange("firstName", e.target.value)}
className="w-full border border-border px-4 py-2 rounded"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Last Name</label>
<input
type="text"
required
value={billingAddress.lastName}
onChange={(e) => handleBillingChange("lastName", e.target.value)}
className="w-full border border-border px-4 py-2 rounded"
/>
</div>
<div className="col-span-2">
<label className="block text-sm font-medium mb-1">Street Address</label>
<input
type="text"
required
value={billingAddress.streetAddress1}
onChange={(e) => handleBillingChange("streetAddress1", e.target.value)}
className="w-full border border-border px-4 py-2 rounded"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">City</label>
<input
type="text"
required
value={billingAddress.city}
onChange={(e) => handleBillingChange("city", e.target.value)}
className="w-full border border-border px-4 py-2 rounded"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Postal Code</label>
<input
type="text"
required
value={billingAddress.postalCode}
onChange={(e) => handleBillingChange("postalCode", e.target.value)}
className="w-full border border-border px-4 py-2 rounded"
/>
</div>
<div className="col-span-2">
<label className="block text-sm font-medium mb-1">Phone</label>
<input
type="tel"
required
value={billingAddress.phone}
onChange={(e) => handleBillingChange("phone", e.target.value)}
className="w-full border border-border px-4 py-2 rounded"
/>
</div>
</div>
</div>
)}
{/* Payment Method */}
<div className="border-b border-border pb-6">
<h2 className="text-xl font-serif mb-4">Payment Method</h2>
<div className="bg-background-ice p-4 rounded">
<div className="flex items-center gap-3">
<input
type="radio"
checked
readOnly
className="w-4 h-4"
/>
<span>Cash on Delivery (COD)</span>
</div>
<p className="text-sm text-foreground-muted mt-2 ml-7">
Pay when your order is delivered to your door.
</p>
</div>
</div>
{/* Submit Button */}
<button
type="submit"
disabled={isLoading || lines.length === 0}
className="w-full py-4 bg-foreground text-white font-medium hover:bg-accent-dark transition-colors disabled:opacity-50"
>
{isLoading ? "Processing..." : `Complete Order - ${formatPrice(total)}`}
</button>
</form>
</div>
{/* Order Summary */}
<div className="bg-background-ice p-6 rounded-lg h-fit">
<h2 className="text-xl font-serif mb-6">Order Summary</h2>
{lines.length === 0 ? (
<p className="text-foreground-muted">Your cart is empty</p>
) : (
<>
<div className="space-y-4 mb-6">
{lines.map((line) => (
<div key={line.id} className="flex gap-4">
<div className="w-16 h-16 bg-white relative flex-shrink-0">
{line.variant.product.media[0]?.url && (
<Image
src={line.variant.product.media[0].url}
alt={line.variant.product.name}
fill
className="object-cover"
/>
)}
</div>
<div className="flex-1">
<h3 className="font-medium text-sm">{line.variant.product.name}</h3>
<p className="text-foreground-muted text-sm">
Qty: {line.quantity}
</p>
<p className="text-sm">
{formatPrice(line.totalPrice.gross.amount)}
</p>
</div>
</div>
))}
</div>
<div className="border-t border-border pt-4 space-y-2">
<div className="flex justify-between">
<span className="text-foreground-muted">Subtotal</span>
<span>{formatPrice(checkout?.subtotalPrice?.gross?.amount || 0)}</span>
</div>
<div className="flex justify-between">
<span className="text-foreground-muted">Shipping</span>
<span>
{checkout?.shippingPrice?.gross?.amount
? formatPrice(checkout.shippingPrice.gross.amount)
: "Calculated"
}
</span>
</div>
<div className="flex justify-between font-medium text-lg pt-2 border-t border-border">
<span>Total</span>
<span>{formatPrice(total)}</span>
</div>
</div>
</>
)}
</div>
</div>
</div>
</section>
<Footer />
</main>
);
}

View File

@@ -1,4 +1,4 @@
import { getProducts } from "@/lib/woocommerce";
import { getProducts } from "@/lib/saleor";
import Header from "@/components/layout/Header";
import Footer from "@/components/layout/Footer";
import AnnouncementBar from "@/components/home/AnnouncementBar";
@@ -7,6 +7,7 @@ import StatsSection from "@/components/home/StatsSection";
import FeaturesSection from "@/components/home/FeaturesSection";
import TestimonialsSection from "@/components/home/TestimonialsSection";
import NewsletterSection from "@/components/home/NewsletterSection";
import ProductCard from "@/components/product/ProductCard";
export const metadata = {
title: "ManoonOils - Premium Natural Oils for Hair & Skin",
@@ -17,15 +18,14 @@ export const metadata = {
export default async function Homepage() {
let products: any[] = [];
try {
products = await getProducts();
products = await getProducts("SR");
} catch (e) {
// Fallback for build time when API is unavailable
console.log('Failed to fetch products during build');
}
const featuredProduct = products.find((p) => p.status === "publish");
const publishedProducts = products
.filter((p) => p.status === "publish")
.slice(0, 4);
const featuredProduct = products[0];
const publishedProducts = products.slice(0, 4);
return (
<main className="min-h-screen bg-white">
@@ -61,7 +61,7 @@ export default async function Homepage() {
</p>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8">
{publishedProducts.map((product, index) => (
<ProductCard key={product.id} product={product} index={index} />
<ProductCard key={product.id} product={product} index={index} locale="SR" />
))}
</div>
</div>
@@ -72,6 +72,3 @@ export default async function Homepage() {
</main>
);
}
// Import ProductCard here to avoid circular dependency
import ProductCard from "@/components/product/ProductCard";

View File

@@ -4,30 +4,28 @@ import { motion } from "framer-motion";
import { Star, ShoppingBag } from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import { useCartStore } from "@/stores/cartStore";
import { WooProduct, formatPrice, getProductImage } from "@/lib/woocommerce";
import { useSaleorCheckoutStore } from "@/stores/saleorCheckoutStore";
import type { Product } from "@/types/saleor";
import { getProductPrice, getProductImage, formatPrice } from "@/lib/saleor";
interface NewHeroProps {
featuredProduct?: WooProduct;
featuredProduct?: Product;
}
export default function NewHero({ featuredProduct }: NewHeroProps) {
const { addItem, openCart } = useCartStore();
const { addLine, openCart } = useSaleorCheckoutStore();
const handleAddToCart = () => {
if (featuredProduct) {
addItem({
id: featuredProduct.id,
name: featuredProduct.name,
price: featuredProduct.price,
quantity: 1,
image: getProductImage(featuredProduct),
sku: featuredProduct.sku,
});
const handleAddToCart = async () => {
const variant = featuredProduct?.variants?.[0];
if (variant?.id) {
await addLine(variant.id, 1);
openCart();
}
};
const price = featuredProduct ? getProductPrice(featuredProduct) : "";
const image = featuredProduct ? getProductImage(featuredProduct) : "";
return (
<section className="relative h-screen min-h-[700px] flex flex-col overflow-hidden pt-10">
{/* Background Image */}
@@ -63,7 +61,7 @@ export default function NewHero({ featuredProduct }: NewHeroProps) {
{/* Product Image */}
<div className="relative aspect-square bg-[#E8F4F8]">
<Image
src={getProductImage(featuredProduct)}
src={image}
alt={featuredProduct.name}
fill
className="object-cover"
@@ -89,7 +87,7 @@ export default function NewHero({ featuredProduct }: NewHeroProps) {
{/* Description */}
<p className="text-sm text-[#4A4A4A]/70 mt-1 line-clamp-2">
{featuredProduct.short_description?.replace(/<[^>]*>/g, "") ||
{featuredProduct.description?.replace(/<[^>]*>/g, "").slice(0, 100) ||
"Premium natural oil for hair and skin care"}
</p>
@@ -107,7 +105,7 @@ export default function NewHero({ featuredProduct }: NewHeroProps) {
<div className="flex items-center justify-between mt-4 pt-4 border-t border-[#1A1A1A]/6">
<div>
<span className="text-lg font-medium text-[#1A1A1A]">
{formatPrice(featuredProduct.price)}
{price}
</span>
<span className="text-xs text-[#4A4A4A]/60 ml-2">50ml</span>
</div>

View File

@@ -18,4 +18,3 @@ export const GET_CHECKOUT_BY_ID = gql`
}
${CHECKOUT_FRAGMENT}
`;
`;

View File

@@ -1,129 +0,0 @@
import WooCommerceRestApi from "@woocommerce/woocommerce-rest-api";
// Lazy initialization - only create API client when needed
let apiInstance: WooCommerceRestApi | null = null;
function getApi(): WooCommerceRestApi {
if (!apiInstance) {
const url = process.env.NEXT_PUBLIC_WOOCOMMERCE_URL;
const consumerKey = process.env.NEXT_PUBLIC_WOOCOMMERCE_CONSUMER_KEY;
const consumerSecret = process.env.NEXT_PUBLIC_WOOCOMMERCE_CONSUMER_SECRET;
if (!url || !consumerKey || !consumerSecret) {
throw new Error("WooCommerce API credentials not configured");
}
apiInstance = new WooCommerceRestApi({
url,
consumerKey,
consumerSecret,
version: "wc/v3",
queryStringAuth: true, // Use query string auth instead of basic auth (more reliable)
});
}
return apiInstance;
}
export interface WooProduct {
id: number;
name: string;
slug: string;
price: string;
regular_price: string;
sale_price: string;
description: string;
short_description: string;
status: "publish" | "draft" | "private";
stock_status: "instock" | "outofstock";
images: { id: number; src: string; alt: string }[];
sku: string;
categories: { id: number; name: string; slug: string }[];
meta_data: { key: string; value: string }[];
}
export interface WooCategory {
id: number;
name: string;
slug: string;
description: string;
image: { src: string } | null;
}
export async function getProducts(perPage = 100): Promise<WooProduct[]> {
try {
const api = getApi();
const response = await api.get("products", { per_page: perPage });
return response.data;
} catch (error) {
console.error("Error fetching products:", error);
return [];
}
}
export async function getProduct(id: number): Promise<WooProduct | null> {
try {
const api = getApi();
const response = await api.get(`products/${id}`);
return response.data;
} catch (error) {
console.error(`Error fetching product ${id}:`, error);
return null;
}
}
export async function getProductBySlug(slug: string): Promise<WooProduct | null> {
try {
const api = getApi();
const response = await api.get("products", { slug });
return response.data[0] || null;
} catch (error) {
console.error(`Error fetching product by slug ${slug}:`, error);
return null;
}
}
export async function getCategories(): Promise<WooCategory[]> {
try {
const api = getApi();
const response = await api.get("product-categories", { per_page: 100 });
return response.data;
} catch (error) {
console.error("Error fetching categories:", error);
return [];
}
}
export async function getProductsByCategory(
categoryId: number
): Promise<WooProduct[]> {
try {
const api = getApi();
const response = await api.get("products", {
category: categoryId,
per_page: 100,
});
return response.data;
} catch (error) {
console.error(`Error fetching products for category ${categoryId}:`, error);
return [];
}
}
export function formatPrice(price: string, currency = "RSD"): string {
const num = parseFloat(price);
if (isNaN(num)) return "0 RSD";
return new Intl.NumberFormat("sr-RS", {
style: "currency",
currency: currency,
minimumFractionDigits: 0,
}).format(num);
}
export function getProductImage(product: WooProduct): string {
if (product.images && product.images.length > 0) {
return product.images[0].src;
}
return "/placeholder-product.jpg";
}
export default getApi;

View File

@@ -1,86 +0,0 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
export interface CartItem {
id: number;
name: string;
price: string;
quantity: number;
image: string;
sku: string;
}
interface CartStore {
items: CartItem[];
isOpen: boolean;
addItem: (item: CartItem) => void;
removeItem: (id: number) => void;
updateQuantity: (id: number, quantity: number) => void;
toggleCart: () => void;
openCart: () => void;
closeCart: () => void;
clearCart: () => void;
getTotal: () => number;
getItemCount: () => number;
}
export const useCartStore = create<CartStore>()(
persist(
(set, get) => ({
items: [],
isOpen: false,
addItem: (item) => {
const items = get().items;
const existingItem = items.find((i) => i.id === item.id);
if (existingItem) {
set({
items: items.map((i) =>
i.id === item.id
? { ...i, quantity: i.quantity + item.quantity }
: i
),
});
} else {
set({ items: [...items, item] });
}
set({ isOpen: true });
},
removeItem: (id) => {
set({ items: get().items.filter((i) => i.id !== id) });
},
updateQuantity: (id, quantity) => {
if (quantity <= 0) {
set({ items: get().items.filter((i) => i.id !== id) });
} else {
set({
items: get().items.map((i) =>
i.id === id ? { ...i, quantity } : i
),
});
}
},
toggleCart: () => set({ isOpen: !get().isOpen }),
openCart: () => set({ isOpen: true }),
closeCart: () => set({ isOpen: false }),
clearCart: () => set({ items: [] }),
getTotal: () => {
return get().items.reduce((total, item) => {
return total + parseFloat(item.price) * item.quantity;
}, 0);
},
getItemCount: () => {
return get().items.reduce((count, item) => count + item.quantity, 0);
},
}),
{
name: "manoonoils-cart",
}
)
);