Compare commits
25 Commits
f9e3ec37f8
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f3079801b | ||
|
|
684ab6917d | ||
|
|
6cdde96c61 | ||
|
|
9419ec0af0 | ||
|
|
cc89455967 | ||
|
|
7060d05ae9 | ||
|
|
58287469ac | ||
|
|
29429f7139 | ||
|
|
d92ec369a9 | ||
|
|
76c819848b | ||
|
|
a2298dfa9c | ||
|
|
6ef7aa3c47 | ||
|
|
68382648a8 | ||
|
|
808506832c | ||
|
|
aa3c333192 | ||
|
|
ec8eb4fee4 | ||
|
|
33214002f2 | ||
|
|
696853e988 | ||
|
|
3156e25068 | ||
|
|
7c75205077 | ||
|
|
7dc9a5fbc7 | ||
|
|
99ad4fa1e0 | ||
|
|
c4ac669a09 | ||
|
|
6ba07d7fef | ||
|
|
a7e31b380d |
10
ecosystem.config.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
module.exports = {
|
||||||
|
apps: [
|
||||||
|
{
|
||||||
|
name: "frontend",
|
||||||
|
script: "server.js",
|
||||||
|
cwd: "/var/www/frontend",
|
||||||
|
env: { NODE_ENV: "production" }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
28
fetch.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import express from "express";
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
import cors from "cors";
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const app = express();
|
||||||
|
const DATA_PATH = path.join(__dirname, "public", "data.json");
|
||||||
|
|
||||||
|
app.use(cors());
|
||||||
|
app.use(express.json({ limit: "10mb" }));
|
||||||
|
|
||||||
|
app.get("/api/data", (req, res) => {
|
||||||
|
const raw = fs.readFileSync(DATA_PATH, "utf-8");
|
||||||
|
res.json(JSON.parse(raw));
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post("/api/data", (req, res) => {
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(DATA_PATH, JSON.stringify(req.body, null, 2), "utf-8");
|
||||||
|
res.json({ ok: true });
|
||||||
|
} catch (e) {
|
||||||
|
res.status(500).json({ ok: false, error: e.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.listen(4000, () => console.log("API running on :4000"));
|
||||||
615
package-lock.json
generated
@@ -10,6 +10,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@reduxjs/toolkit": "^2.5.0",
|
"@reduxjs/toolkit": "^2.5.0",
|
||||||
"antd": "^5.22.7",
|
"antd": "^5.22.7",
|
||||||
|
"express": "^5.2.1",
|
||||||
"framer-motion": "^12.5.0",
|
"framer-motion": "^12.5.0",
|
||||||
"i18next": "^24.2.1",
|
"i18next": "^24.2.1",
|
||||||
"i18next-browser-languagedetector": "^8.0.2",
|
"i18next-browser-languagedetector": "^8.0.2",
|
||||||
@@ -1079,17 +1080,6 @@
|
|||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@jridgewell/source-map": {
|
|
||||||
"version": "0.3.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz",
|
|
||||||
"integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==",
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@jridgewell/gen-mapping": "^0.3.5",
|
|
||||||
"@jridgewell/trace-mapping": "^0.3.25"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@jridgewell/sourcemap-codec": {
|
"node_modules/@jridgewell/sourcemap-codec": {
|
||||||
"version": "1.5.0",
|
"version": "1.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
|
||||||
@@ -1624,6 +1614,19 @@
|
|||||||
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0"
|
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/accepts": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-types": "^3.0.0",
|
||||||
|
"negotiator": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/acorn": {
|
"node_modules/acorn": {
|
||||||
"version": "8.14.0",
|
"version": "8.14.0",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
|
||||||
@@ -1897,6 +1900,30 @@
|
|||||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/body-parser": {
|
||||||
|
"version": "2.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz",
|
||||||
|
"integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bytes": "^3.1.2",
|
||||||
|
"content-type": "^1.0.5",
|
||||||
|
"debug": "^4.4.3",
|
||||||
|
"http-errors": "^2.0.0",
|
||||||
|
"iconv-lite": "^0.7.0",
|
||||||
|
"on-finished": "^2.4.1",
|
||||||
|
"qs": "^6.14.1",
|
||||||
|
"raw-body": "^3.0.1",
|
||||||
|
"type-is": "^2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.11",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||||
@@ -1946,12 +1973,14 @@
|
|||||||
"integrity": "sha512-7VPMEPuYznPSoR21NE1zvd2Xna6c/CloiZCfcMXR1Jny6PjX0N4Nsa38zcBFo/FMK+BlA+FLKbJCQ0i2yxp+Xg==",
|
"integrity": "sha512-7VPMEPuYznPSoR21NE1zvd2Xna6c/CloiZCfcMXR1Jny6PjX0N4Nsa38zcBFo/FMK+BlA+FLKbJCQ0i2yxp+Xg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/buffer-from": {
|
"node_modules/bytes": {
|
||||||
"version": "1.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
|
||||||
"dev": true,
|
"license": "MIT",
|
||||||
"optional": true
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"node_modules/call-bind": {
|
"node_modules/call-bind": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
@@ -1975,7 +2004,6 @@
|
|||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz",
|
||||||
"integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==",
|
"integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"es-errors": "^1.3.0",
|
"es-errors": "^1.3.0",
|
||||||
"function-bind": "^1.1.2"
|
"function-bind": "^1.1.2"
|
||||||
@@ -1988,7 +2016,6 @@
|
|||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz",
|
||||||
"integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==",
|
"integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"call-bind-apply-helpers": "^1.0.1",
|
"call-bind-apply-helpers": "^1.0.1",
|
||||||
"get-intrinsic": "^1.2.6"
|
"get-intrinsic": "^1.2.6"
|
||||||
@@ -2074,13 +2101,6 @@
|
|||||||
"integrity": "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==",
|
"integrity": "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/commander": {
|
|
||||||
"version": "2.20.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
|
||||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
|
|
||||||
"dev": true,
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"node_modules/compute-scroll-into-view": {
|
"node_modules/compute-scroll-into-view": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.0.tgz",
|
||||||
@@ -2092,6 +2112,28 @@
|
|||||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/content-disposition": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/content-type": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/convert-source-map": {
|
"node_modules/convert-source-map": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
||||||
@@ -2106,6 +2148,15 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cookie-signature": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/copy-to-clipboard": {
|
"node_modules/copy-to-clipboard": {
|
||||||
"version": "3.3.3",
|
"version": "3.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz",
|
||||||
@@ -2201,10 +2252,10 @@
|
|||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.0",
|
"version": "4.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
|
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||||
"dev": true,
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ms": "^2.1.3"
|
"ms": "^2.1.3"
|
||||||
},
|
},
|
||||||
@@ -2257,6 +2308,15 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/depd": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/doctrine": {
|
"node_modules/doctrine": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
|
||||||
@@ -2273,7 +2333,6 @@
|
|||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"call-bind-apply-helpers": "^1.0.1",
|
"call-bind-apply-helpers": "^1.0.1",
|
||||||
"es-errors": "^1.3.0",
|
"es-errors": "^1.3.0",
|
||||||
@@ -2283,12 +2342,27 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ee-first": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.5.76",
|
"version": "1.5.76",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.76.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.76.tgz",
|
||||||
"integrity": "sha512-CjVQyG7n7Sr+eBXE86HIulnL5N8xZY1sgmOPGuq/F0Rr0FJq63lg0kEtOIDfZBk44FnDLf6FUJ+dsJcuiUDdDQ==",
|
"integrity": "sha512-CjVQyG7n7Sr+eBXE86HIulnL5N8xZY1sgmOPGuq/F0Rr0FJq63lg0kEtOIDfZBk44FnDLf6FUJ+dsJcuiUDdDQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/encodeurl": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/es-abstract": {
|
"node_modules/es-abstract": {
|
||||||
"version": "1.23.9",
|
"version": "1.23.9",
|
||||||
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz",
|
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz",
|
||||||
@@ -2358,7 +2432,6 @@
|
|||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
@@ -2367,7 +2440,6 @@
|
|||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
@@ -2403,7 +2475,6 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz",
|
||||||
"integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==",
|
"integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"es-errors": "^1.3.0"
|
"es-errors": "^1.3.0"
|
||||||
},
|
},
|
||||||
@@ -2501,6 +2572,12 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/escape-html": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/escape-string-regexp": {
|
"node_modules/escape-string-regexp": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||||
@@ -2713,6 +2790,67 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/etag": {
|
||||||
|
"version": "1.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
||||||
|
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/express": {
|
||||||
|
"version": "5.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
|
||||||
|
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"accepts": "^2.0.0",
|
||||||
|
"body-parser": "^2.2.1",
|
||||||
|
"content-disposition": "^1.0.0",
|
||||||
|
"content-type": "^1.0.5",
|
||||||
|
"cookie": "^0.7.1",
|
||||||
|
"cookie-signature": "^1.2.1",
|
||||||
|
"debug": "^4.4.0",
|
||||||
|
"depd": "^2.0.0",
|
||||||
|
"encodeurl": "^2.0.0",
|
||||||
|
"escape-html": "^1.0.3",
|
||||||
|
"etag": "^1.8.1",
|
||||||
|
"finalhandler": "^2.1.0",
|
||||||
|
"fresh": "^2.0.0",
|
||||||
|
"http-errors": "^2.0.0",
|
||||||
|
"merge-descriptors": "^2.0.0",
|
||||||
|
"mime-types": "^3.0.0",
|
||||||
|
"on-finished": "^2.4.1",
|
||||||
|
"once": "^1.4.0",
|
||||||
|
"parseurl": "^1.3.3",
|
||||||
|
"proxy-addr": "^2.0.7",
|
||||||
|
"qs": "^6.14.0",
|
||||||
|
"range-parser": "^1.2.1",
|
||||||
|
"router": "^2.2.0",
|
||||||
|
"send": "^1.1.0",
|
||||||
|
"serve-static": "^2.2.0",
|
||||||
|
"statuses": "^2.0.1",
|
||||||
|
"type-is": "^2.0.1",
|
||||||
|
"vary": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/express/node_modules/cookie": {
|
||||||
|
"version": "0.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||||
|
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fast-deep-equal": {
|
"node_modules/fast-deep-equal": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
@@ -2743,6 +2881,27 @@
|
|||||||
"node": ">=16.0.0"
|
"node": ">=16.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/finalhandler": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "^4.4.0",
|
||||||
|
"encodeurl": "^2.0.0",
|
||||||
|
"escape-html": "^1.0.3",
|
||||||
|
"on-finished": "^2.4.1",
|
||||||
|
"parseurl": "^1.3.3",
|
||||||
|
"statuses": "^2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/find-up": {
|
"node_modules/find-up": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
|
||||||
@@ -2787,6 +2946,15 @@
|
|||||||
"is-callable": "^1.1.3"
|
"is-callable": "^1.1.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/forwarded": {
|
||||||
|
"version": "0.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||||
|
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/framer-motion": {
|
"node_modules/framer-motion": {
|
||||||
"version": "12.5.0",
|
"version": "12.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.5.0.tgz",
|
||||||
@@ -2813,6 +2981,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fresh": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fsevents": {
|
"node_modules/fsevents": {
|
||||||
"version": "2.3.3",
|
"version": "2.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
@@ -2831,7 +3008,6 @@
|
|||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||||
"dev": true,
|
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
@@ -2878,7 +3054,6 @@
|
|||||||
"version": "1.2.7",
|
"version": "1.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz",
|
||||||
"integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==",
|
"integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"call-bind-apply-helpers": "^1.0.1",
|
"call-bind-apply-helpers": "^1.0.1",
|
||||||
"es-define-property": "^1.0.1",
|
"es-define-property": "^1.0.1",
|
||||||
@@ -2902,7 +3077,6 @@
|
|||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dunder-proto": "^1.0.1",
|
"dunder-proto": "^1.0.1",
|
||||||
"es-object-atoms": "^1.0.0"
|
"es-object-atoms": "^1.0.0"
|
||||||
@@ -2972,7 +3146,6 @@
|
|||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
},
|
},
|
||||||
@@ -3032,7 +3205,6 @@
|
|||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
},
|
},
|
||||||
@@ -3059,7 +3231,6 @@
|
|||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"function-bind": "^1.1.2"
|
"function-bind": "^1.1.2"
|
||||||
},
|
},
|
||||||
@@ -3075,6 +3246,26 @@
|
|||||||
"void-elements": "3.1.0"
|
"void-elements": "3.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/http-errors": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"depd": "~2.0.0",
|
||||||
|
"inherits": "~2.0.4",
|
||||||
|
"setprototypeof": "~1.2.0",
|
||||||
|
"statuses": "~2.0.2",
|
||||||
|
"toidentifier": "~1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/i18next": {
|
"node_modules/i18next": {
|
||||||
"version": "24.2.1",
|
"version": "24.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-24.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/i18next/-/i18next-24.2.1.tgz",
|
||||||
@@ -3114,6 +3305,22 @@
|
|||||||
"@babel/runtime": "^7.23.2"
|
"@babel/runtime": "^7.23.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/iconv-lite": {
|
||||||
|
"version": "0.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
|
||||||
|
"integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ignore": {
|
"node_modules/ignore": {
|
||||||
"version": "5.3.2",
|
"version": "5.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||||
@@ -3174,6 +3381,12 @@
|
|||||||
"node": ">=0.8.19"
|
"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/install": {
|
"node_modules/install": {
|
||||||
"version": "0.13.0",
|
"version": "0.13.0",
|
||||||
"resolved": "https://registry.npmjs.org/install/-/install-0.13.0.tgz",
|
"resolved": "https://registry.npmjs.org/install/-/install-0.13.0.tgz",
|
||||||
@@ -3197,6 +3410,15 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ipaddr.js": {
|
||||||
|
"version": "1.9.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||||
|
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-array-buffer": {
|
"node_modules/is-array-buffer": {
|
||||||
"version": "3.0.5",
|
"version": "3.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
|
||||||
@@ -3405,6 +3627,12 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-promise": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/is-regex": {
|
"node_modules/is-regex": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
|
||||||
@@ -3732,11 +3960,56 @@
|
|||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/media-typer": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/merge-descriptors": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mime-db": {
|
||||||
|
"version": "1.54.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
|
||||||
|
"integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mime-types": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-db": "^1.54.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/minimatch": {
|
"node_modules/minimatch": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||||
@@ -3765,8 +4038,7 @@
|
|||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.8",
|
"version": "3.3.8",
|
||||||
@@ -3792,6 +4064,15 @@
|
|||||||
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
|
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/negotiator": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/node-releases": {
|
"node_modules/node-releases": {
|
||||||
"version": "2.0.19",
|
"version": "2.0.19",
|
||||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
|
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
|
||||||
@@ -6474,7 +6755,6 @@
|
|||||||
"version": "1.13.3",
|
"version": "1.13.3",
|
||||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz",
|
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz",
|
||||||
"integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==",
|
"integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==",
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
},
|
},
|
||||||
@@ -6561,6 +6841,27 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/on-finished": {
|
||||||
|
"version": "2.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||||
|
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ee-first": "1.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/once": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"wrappy": "1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/optionator": {
|
"node_modules/optionator": {
|
||||||
"version": "0.9.4",
|
"version": "0.9.4",
|
||||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
||||||
@@ -6637,6 +6938,15 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/parseurl": {
|
||||||
|
"version": "1.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||||
|
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/path-exists": {
|
"node_modules/path-exists": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||||
@@ -6661,6 +6971,16 @@
|
|||||||
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
|
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/path-to-regexp": {
|
||||||
|
"version": "8.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.0.tgz",
|
||||||
|
"integrity": "sha512-PuseHIvAnz3bjrM2rGJtSgo1zjgxapTLZ7x2pjhzWwlp4SJQgK3f3iZIQwkpEnBaKz6seKBADpM4B4ySkuYypg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/picocolors": {
|
"node_modules/picocolors": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
@@ -6724,6 +7044,19 @@
|
|||||||
"react-is": "^16.13.1"
|
"react-is": "^16.13.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/proxy-addr": {
|
||||||
|
"version": "2.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||||
|
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"forwarded": "0.2.0",
|
||||||
|
"ipaddr.js": "1.9.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/punycode": {
|
"node_modules/punycode": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||||
@@ -6733,6 +7066,45 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/qs": {
|
||||||
|
"version": "6.15.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz",
|
||||||
|
"integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"side-channel": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/range-parser": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/raw-body": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bytes": "~3.1.2",
|
||||||
|
"http-errors": "~2.0.1",
|
||||||
|
"iconv-lite": "~0.7.0",
|
||||||
|
"unpipe": "~1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/rc-cascader": {
|
"node_modules/rc-cascader": {
|
||||||
"version": "3.30.0",
|
"version": "3.30.0",
|
||||||
"resolved": "https://registry.npmjs.org/rc-cascader/-/rc-cascader-3.30.0.tgz",
|
"resolved": "https://registry.npmjs.org/rc-cascader/-/rc-cascader-3.30.0.tgz",
|
||||||
@@ -7595,6 +7967,22 @@
|
|||||||
"fsevents": "~2.3.2"
|
"fsevents": "~2.3.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/router": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "^4.4.0",
|
||||||
|
"depd": "^2.0.0",
|
||||||
|
"is-promise": "^4.0.0",
|
||||||
|
"parseurl": "^1.3.3",
|
||||||
|
"path-to-regexp": "^8.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/rxjs": {
|
"node_modules/rxjs": {
|
||||||
"version": "7.8.1",
|
"version": "7.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
|
||||||
@@ -7656,6 +8044,12 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/safer-buffer": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/sass-embedded": {
|
"node_modules/sass-embedded": {
|
||||||
"version": "1.83.0",
|
"version": "1.83.0",
|
||||||
"resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.83.0.tgz",
|
"resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.83.0.tgz",
|
||||||
@@ -8060,6 +8454,51 @@
|
|||||||
"semver": "bin/semver.js"
|
"semver": "bin/semver.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/send": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "^4.4.3",
|
||||||
|
"encodeurl": "^2.0.0",
|
||||||
|
"escape-html": "^1.0.3",
|
||||||
|
"etag": "^1.8.1",
|
||||||
|
"fresh": "^2.0.0",
|
||||||
|
"http-errors": "^2.0.1",
|
||||||
|
"mime-types": "^3.0.2",
|
||||||
|
"ms": "^2.1.3",
|
||||||
|
"on-finished": "^2.4.1",
|
||||||
|
"range-parser": "^1.2.1",
|
||||||
|
"statuses": "^2.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/serve-static": {
|
||||||
|
"version": "2.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz",
|
||||||
|
"integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"encodeurl": "^2.0.0",
|
||||||
|
"escape-html": "^1.0.3",
|
||||||
|
"parseurl": "^1.3.3",
|
||||||
|
"send": "^1.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/set-cookie-parser": {
|
"node_modules/set-cookie-parser": {
|
||||||
"version": "2.7.1",
|
"version": "2.7.1",
|
||||||
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
|
||||||
@@ -8111,6 +8550,12 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/setprototypeof": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/shebang-command": {
|
"node_modules/shebang-command": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||||
@@ -8136,7 +8581,6 @@
|
|||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
|
||||||
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
|
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"es-errors": "^1.3.0",
|
"es-errors": "^1.3.0",
|
||||||
"object-inspect": "^1.13.3",
|
"object-inspect": "^1.13.3",
|
||||||
@@ -8155,7 +8599,6 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
|
||||||
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
|
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"es-errors": "^1.3.0",
|
"es-errors": "^1.3.0",
|
||||||
"object-inspect": "^1.13.3"
|
"object-inspect": "^1.13.3"
|
||||||
@@ -8171,7 +8614,6 @@
|
|||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
|
||||||
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
|
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"call-bound": "^1.0.2",
|
"call-bound": "^1.0.2",
|
||||||
"es-errors": "^1.3.0",
|
"es-errors": "^1.3.0",
|
||||||
@@ -8189,7 +8631,6 @@
|
|||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
|
||||||
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
|
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"call-bound": "^1.0.2",
|
"call-bound": "^1.0.2",
|
||||||
"es-errors": "^1.3.0",
|
"es-errors": "^1.3.0",
|
||||||
@@ -8204,16 +8645,6 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/source-map": {
|
|
||||||
"version": "0.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
|
||||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/source-map-js": {
|
"node_modules/source-map-js": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
@@ -8223,15 +8654,13 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/source-map-support": {
|
"node_modules/statuses": {
|
||||||
"version": "0.5.21",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
|
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||||
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
|
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
|
||||||
"dev": true,
|
"license": "MIT",
|
||||||
"optional": true,
|
"engines": {
|
||||||
"dependencies": {
|
"node": ">= 0.8"
|
||||||
"buffer-from": "^1.0.0",
|
|
||||||
"source-map": "^0.6.0"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/string-convert": {
|
"node_modules/string-convert": {
|
||||||
@@ -8425,6 +8854,15 @@
|
|||||||
"resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz",
|
||||||
"integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ=="
|
"integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/toidentifier": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tslib": {
|
"node_modules/tslib": {
|
||||||
"version": "2.8.1",
|
"version": "2.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
@@ -8447,6 +8885,20 @@
|
|||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/type-is": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"content-type": "^1.0.5",
|
||||||
|
"media-typer": "^1.1.0",
|
||||||
|
"mime-types": "^3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/typed-array-buffer": {
|
"node_modules/typed-array-buffer": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz",
|
||||||
@@ -8539,12 +8991,14 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/undici-types": {
|
"node_modules/unpipe": {
|
||||||
"version": "6.20.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
|
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||||
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
|
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
|
||||||
"dev": true,
|
"license": "MIT",
|
||||||
"optional": true
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"node_modules/update-browserslist-db": {
|
"node_modules/update-browserslist-db": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
@@ -8599,6 +9053,15 @@
|
|||||||
"integrity": "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==",
|
"integrity": "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/vary": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "6.0.7",
|
"version": "6.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-6.0.7.tgz",
|
||||||
@@ -8787,6 +9250,12 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/wrappy": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/yallist": {
|
"node_modules/yallist": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||||
|
|||||||
@@ -7,11 +7,12 @@
|
|||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"preview": "vite preview"
|
"preview": "node server.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@reduxjs/toolkit": "^2.5.0",
|
"@reduxjs/toolkit": "^2.5.0",
|
||||||
"antd": "^5.22.7",
|
"antd": "^5.22.7",
|
||||||
|
"express": "^5.2.1",
|
||||||
"framer-motion": "^12.5.0",
|
"framer-motion": "^12.5.0",
|
||||||
"i18next": "^24.2.1",
|
"i18next": "^24.2.1",
|
||||||
"i18next-browser-languagedetector": "^8.0.2",
|
"i18next-browser-languagedetector": "^8.0.2",
|
||||||
|
|||||||
1
public/data.json
Normal file
41
server.js
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import express from "express";
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const port = 4173;
|
||||||
|
|
||||||
|
// API endpointleri
|
||||||
|
app.post("/frontend-api/data", express.json({ limit: '20mb' }), (req, res) => {
|
||||||
|
fs.writeFile(path.join(process.cwd(), "dist", "data.json"), JSON.stringify(req.body, null, 2), (err) => {
|
||||||
|
if (err) {
|
||||||
|
res.status(500).send(err.message);
|
||||||
|
} else {
|
||||||
|
res.status(200).json({ ok: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/frontend-api/data", (req, res) => {
|
||||||
|
fs.readFile(path.join(process.cwd(), "dist", "data.json"), "utf-8", (err, data) => {
|
||||||
|
if (err) {
|
||||||
|
res.status(500).send(err.message);
|
||||||
|
} else {
|
||||||
|
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
|
||||||
|
res.setHeader("Pragma", "no-cache");
|
||||||
|
res.setHeader("Expires", "0");
|
||||||
|
res.setHeader("Content-Type", "application/json");
|
||||||
|
res.send(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Statik dosyaları sun ve diğer tüm istekleri index.html'e yönlendir
|
||||||
|
app.use(express.static(path.join(process.cwd(), "dist")));
|
||||||
|
app.get(/^(?!\/api).*/, (req, res) => {
|
||||||
|
res.sendFile(path.join(process.cwd(), "dist", "index.html"));
|
||||||
|
});
|
||||||
|
|
||||||
|
app.listen(port, () => {
|
||||||
|
console.log(`Sunucu http://localhost:${port} adresinde çalışıyor`);
|
||||||
|
});
|
||||||
@@ -8,7 +8,10 @@ export const mediaApi = baseApi.injectEndpoints({
|
|||||||
getBanners: builder.query({
|
getBanners: builder.query({
|
||||||
query: () => '/media/banners',
|
query: () => '/media/banners',
|
||||||
}),
|
}),
|
||||||
|
getStories: builder.query({
|
||||||
|
query: () => '/media/stories',
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const { useGetCarouselsQuery, useGetBannersQuery } = mediaApi;
|
export const { useGetCarouselsQuery, useGetBannersQuery, useGetStoriesQuery } = mediaApi;
|
||||||
@@ -5,16 +5,9 @@ export const brandsApi = baseApi.injectEndpoints({
|
|||||||
getBrands: builder.query({
|
getBrands: builder.query({
|
||||||
query: (params = {}) => {
|
query: (params = {}) => {
|
||||||
const queryParams = new URLSearchParams();
|
const queryParams = new URLSearchParams();
|
||||||
if (params.type) {
|
if (params.type) queryParams.append("type", params.type);
|
||||||
queryParams.append("type", params.type);
|
if (params.page) queryParams.append("page", params.page);
|
||||||
}
|
if (params.perPage) queryParams.append("perPage", params.perPage);
|
||||||
if (params.page) {
|
|
||||||
queryParams.append("page", params.page);
|
|
||||||
}
|
|
||||||
if (params.limit) {
|
|
||||||
queryParams.append("limit", params.limit);
|
|
||||||
}
|
|
||||||
|
|
||||||
const queryString = queryParams.toString();
|
const queryString = queryParams.toString();
|
||||||
return `/brands${queryString ? `?${queryString}` : ""}`;
|
return `/brands${queryString ? `?${queryString}` : ""}`;
|
||||||
},
|
},
|
||||||
@@ -28,18 +21,19 @@ export const brandsApi = baseApi.injectEndpoints({
|
|||||||
|
|
||||||
getBrandProducts: builder.query({
|
getBrandProducts: builder.query({
|
||||||
query: (params) => {
|
query: (params) => {
|
||||||
if (typeof params === 'string' || typeof params === 'number') {
|
if (typeof params === "string" || typeof params === "number") {
|
||||||
return `/brands/${params}/products`;
|
return `/brands/${params}/products`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { id, page = 1, limit } = params;
|
const { id, page = 1, perPage = 12, sorting, min_price, max_price } = params;
|
||||||
let url = `/brands/${id}/products?page=${page}`;
|
const urlParams = new URLSearchParams();
|
||||||
|
urlParams.append("page", page);
|
||||||
|
urlParams.append("perPage", perPage);
|
||||||
|
if (sorting) urlParams.append("sorting", sorting);
|
||||||
|
if (min_price) urlParams.append("min_price", min_price);
|
||||||
|
if (max_price) urlParams.append("max_price", max_price);
|
||||||
|
|
||||||
if (limit) {
|
return `/brands/${id}/products?${urlParams.toString()}`;
|
||||||
url += `&limit=${limit}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return url;
|
|
||||||
},
|
},
|
||||||
transformResponse: (response) => ({
|
transformResponse: (response) => ({
|
||||||
data: response.data || response,
|
data: response.data || response,
|
||||||
|
|||||||
@@ -7,14 +7,14 @@ export const categoriesApi = baseApi.injectEndpoints({
|
|||||||
}),
|
}),
|
||||||
|
|
||||||
getCategoryProducts: builder.query({
|
getCategoryProducts: builder.query({
|
||||||
query: ({ categoryId, page = 1, limit, brands, min_price, max_price }) => {
|
query: ({ categoryId, page = 1, perPage = 12, brands, min_price, max_price, sorting }) => {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
params.append('page', page);
|
params.append("page", page);
|
||||||
if (limit) params.append('limit', limit);
|
params.append("perPage", perPage);
|
||||||
if (brands) params.append('brands', brands);
|
if (brands) params.append("brands", brands);
|
||||||
if (min_price) params.append('min_price', min_price);
|
if (min_price) params.append("min_price", min_price);
|
||||||
if (max_price) params.append('max_price', max_price);
|
if (max_price) params.append("max_price", max_price);
|
||||||
|
if (sorting) params.append("sorting", sorting);
|
||||||
return `categories/${categoryId}/products?${params.toString()}`;
|
return `categories/${categoryId}/products?${params.toString()}`;
|
||||||
},
|
},
|
||||||
transformResponse: (response) => ({
|
transformResponse: (response) => ({
|
||||||
@@ -24,77 +24,103 @@ export const categoriesApi = baseApi.injectEndpoints({
|
|||||||
}),
|
}),
|
||||||
|
|
||||||
getAllCategoryProducts: builder.query({
|
getAllCategoryProducts: builder.query({
|
||||||
async queryFn(category, queryApi, extraOptions, baseQuery) {
|
async queryFn(category, _queryApi, _extraOptions, baseQuery) {
|
||||||
const fetchProducts = async (categoryId) => {
|
const fetchProducts = async (categoryId) => {
|
||||||
const result = await baseQuery(`categories/${categoryId}/products`);
|
const result = await baseQuery(`categories/${categoryId}/products`);
|
||||||
return result.data ? result.data.data : [];
|
return result.data ? result.data.data : [];
|
||||||
};
|
};
|
||||||
|
|
||||||
let allProducts = await fetchProducts(category.id);
|
let allProducts = await fetchProducts(category.id);
|
||||||
|
|
||||||
for (const child of category.children) {
|
for (const child of category.children) {
|
||||||
const childProducts = await fetchProducts(child.id);
|
const childProducts = await fetchProducts(child.id);
|
||||||
allProducts = [...allProducts, ...childProducts];
|
allProducts = [...allProducts, ...childProducts];
|
||||||
}
|
}
|
||||||
|
|
||||||
return { data: allProducts };
|
return { data: allProducts };
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getAllCategoryProductsPaginated: builder.query({
|
getAllCategoryProductsPaginated: builder.query({
|
||||||
async queryFn(
|
async queryFn(
|
||||||
{ category, page = 1, limit = 6, brands, min_price, max_price },
|
{ category, page = 1, perPage = 12, brands, min_price, max_price, sorting },
|
||||||
queryApi,
|
_queryApi,
|
||||||
extraOptions,
|
_extraOptions,
|
||||||
baseQuery
|
baseQuery
|
||||||
) {
|
) {
|
||||||
if (!category) return { data: [] };
|
if (!category) return { data: { data: [], pagination: { currentPage: 1, hasMorePages: false } } };
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const hasMoreByCategory = {};
|
|
||||||
|
|
||||||
const fetchProductsForPage = async (categoryIds, currentPage) => {
|
|
||||||
let allPageProducts = [];
|
|
||||||
const perCategoryLimit = Math.ceil(limit / categoryIds.length);
|
|
||||||
|
|
||||||
for (const categoryId of categoryIds) {
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
params.append('page', currentPage);
|
|
||||||
params.append('limit', perCategoryLimit);
|
|
||||||
if (brands) params.append('brands', brands);
|
|
||||||
if (min_price) params.append('min_price', min_price);
|
|
||||||
if (max_price) params.append('max_price', max_price);
|
|
||||||
|
|
||||||
const result = await baseQuery(
|
|
||||||
`categories/${categoryId}/products?${params.toString()}`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result.data && result.data.data) {
|
|
||||||
allPageProducts = [...allPageProducts, ...result.data.data];
|
|
||||||
hasMoreByCategory[categoryId] = !!result.data.pagination.next_page_url;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return allPageProducts;
|
|
||||||
};
|
|
||||||
|
|
||||||
const categoryIds = [category.id];
|
const categoryIds = [category.id];
|
||||||
if (category.children && category.children.length > 0) {
|
if (category.children?.length > 0) {
|
||||||
category.children.forEach((child) => categoryIds.push(child.id));
|
category.children.forEach((child) => categoryIds.push(child.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
const productsForPage = await fetchProductsForPage(categoryIds, page);
|
// Tek category — direkt fetch, limit tam uygulanır
|
||||||
|
if (categoryIds.length === 1) {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
params.append("page", page);
|
||||||
|
params.append("perPage", perPage);
|
||||||
|
if (brands) params.append("brands", brands);
|
||||||
|
if (min_price) params.append("min_price", min_price);
|
||||||
|
if (max_price) params.append("max_price", max_price);
|
||||||
|
if (sorting) params.append("sorting", sorting);
|
||||||
|
|
||||||
const hasMorePages = Object.values(hasMoreByCategory).some(
|
const result = await baseQuery(
|
||||||
(hasMore) => hasMore
|
`categories/${categoryIds[0]}/products?${params.toString()}`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (result.error) return { error: result.error };
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: {
|
data: {
|
||||||
data: productsForPage,
|
data: result.data?.data || [],
|
||||||
pagination: {
|
pagination: {
|
||||||
currentPage: page,
|
currentPage: page,
|
||||||
hasMorePages: hasMorePages,
|
hasMorePages: !!result.data?.pagination?.next_page_url,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Birden fazla category — paralel fetch, her biri tam limit ile
|
||||||
|
// Sonra client-side deduplicate + slice
|
||||||
|
const requests = categoryIds.map((categoryId) => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
params.append("page", page);
|
||||||
|
params.append("perPage", perPage);
|
||||||
|
if (brands) params.append("brands", brands);
|
||||||
|
if (min_price) params.append("min_price", min_price);
|
||||||
|
if (max_price) params.append("max_price", max_price);
|
||||||
|
if (sorting) params.append("sorting", sorting);
|
||||||
|
|
||||||
|
return baseQuery(`categories/${categoryId}/products?${params.toString()}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
const results = await Promise.all(requests);
|
||||||
|
|
||||||
|
let allProducts = [];
|
||||||
|
let hasMorePages = false;
|
||||||
|
const seenIds = new Set();
|
||||||
|
|
||||||
|
for (const result of results) {
|
||||||
|
if (result.error) continue;
|
||||||
|
const items = result.data?.data || [];
|
||||||
|
for (const item of items) {
|
||||||
|
if (!seenIds.has(item.id)) {
|
||||||
|
seenIds.add(item.id);
|
||||||
|
allProducts.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (result.data?.pagination?.next_page_url) {
|
||||||
|
hasMorePages = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
data: allProducts,
|
||||||
|
pagination: {
|
||||||
|
currentPage: page,
|
||||||
|
hasMorePages,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
53
src/app/api/channelsApi.js
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { baseApi } from "./baseApi";
|
||||||
|
|
||||||
|
export const channelsApi = baseApi.injectEndpoints({
|
||||||
|
endpoints: (builder) => ({
|
||||||
|
getChannelProducts: builder.query({
|
||||||
|
query: (params) => {
|
||||||
|
// params: { channelId, page, limit, min_price, max_price, sorting }
|
||||||
|
const {
|
||||||
|
channelId,
|
||||||
|
page = 1,
|
||||||
|
perPage = 24,
|
||||||
|
min_price,
|
||||||
|
max_price,
|
||||||
|
sorting,
|
||||||
|
} = params;
|
||||||
|
const urlParams = new URLSearchParams();
|
||||||
|
urlParams.append("page", page);
|
||||||
|
urlParams.append("perPage", perPage);
|
||||||
|
if (min_price) urlParams.append("min_price", min_price);
|
||||||
|
if (max_price) urlParams.append("max_price", max_price);
|
||||||
|
if (sorting) urlParams.append("sorting", sorting);
|
||||||
|
return `/channels/${channelId}/products?${urlParams.toString()}`;
|
||||||
|
},
|
||||||
|
transformResponse: (response) => ({
|
||||||
|
data: response.data || response,
|
||||||
|
pagination: response.pagination || {},
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
|
||||||
|
getChannels: builder.query({
|
||||||
|
query: (params = {}) => {
|
||||||
|
const queryParams = new URLSearchParams();
|
||||||
|
if (params.page) queryParams.append("page", params.page);
|
||||||
|
if (params.perPage) queryParams.append("perPage", params.perPage);
|
||||||
|
if (params.search) queryParams.append("search", params.search);
|
||||||
|
const queryString = queryParams.toString();
|
||||||
|
return `/channels${queryString ? `?${queryString}` : ""}`;
|
||||||
|
},
|
||||||
|
transformResponse: (response) => ({
|
||||||
|
data: response.data || response,
|
||||||
|
pagination: response.pagination || {},
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
overrideExisting: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const {
|
||||||
|
useGetChannelProductsQuery,
|
||||||
|
useLazyGetChannelProductsQuery,
|
||||||
|
useGetChannelsQuery,
|
||||||
|
useLazyGetChannelsQuery,
|
||||||
|
} = channelsApi;
|
||||||
@@ -12,32 +12,28 @@ export const collectionsApi = baseApi.injectEndpoints({
|
|||||||
|
|
||||||
getCollectionProducts: builder.query({
|
getCollectionProducts: builder.query({
|
||||||
query: (collectionId) => `/collections/${collectionId}/products`,
|
query: (collectionId) => `/collections/${collectionId}/products`,
|
||||||
transformResponse: (response) => {
|
transformResponse: (response) => ({
|
||||||
return {
|
|
||||||
data: response.data || [],
|
data: response.data || [],
|
||||||
isEmpty: !response.data || response.data.length === 0,
|
isEmpty: !response.data || response.data.length === 0,
|
||||||
};
|
}),
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
checkCollectionHasProducts: builder.query({
|
checkCollectionHasProducts: builder.query({
|
||||||
query: (collectionId) => `/collections/${collectionId}/products?limit=1`,
|
query: (collectionId) => `/collections/${collectionId}/products`,
|
||||||
transformResponse: (response) => {
|
transformResponse: (response) => ({
|
||||||
return {
|
|
||||||
hasProducts: response.data && response.data.length > 0,
|
hasProducts: response.data && response.data.length > 0,
|
||||||
};
|
}),
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getCollectionProductsPaginated: builder.query({
|
getCollectionProductsPaginated: builder.query({
|
||||||
query: ({ collectionId, page = 1, limit = 6, brands, min_price, max_price }) => {
|
query: ({ collectionId, page = 1, perPage = 24, brands, min_price, max_price, sorting }) => {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
params.append('page', page);
|
params.append("page", page);
|
||||||
if (limit) params.append('limit', limit);
|
params.append("perPage", perPage);
|
||||||
if (brands) params.append('brands', brands);
|
if (brands) params.append("brands", brands);
|
||||||
if (min_price) params.append('min_price', min_price);
|
if (min_price) params.append("min_price", min_price);
|
||||||
if (max_price) params.append('max_price', max_price);
|
if (max_price) params.append("max_price", max_price);
|
||||||
|
if (sorting) params.append("sorting", sorting); // undefined gelirse gönderme
|
||||||
return `/collections/${collectionId}/products?${params.toString()}`;
|
return `/collections/${collectionId}/products?${params.toString()}`;
|
||||||
},
|
},
|
||||||
transformResponse: (response) => ({
|
transformResponse: (response) => ({
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ export const filtersApi = baseApi.injectEndpoints({
|
|||||||
if (params?.brand_id) {
|
if (params?.brand_id) {
|
||||||
queryParams.append("brand_id", String(params.brand_id))
|
queryParams.append("brand_id", String(params.brand_id))
|
||||||
}
|
}
|
||||||
|
if (params?.channel_id) {
|
||||||
|
queryParams.append("channel_id", String(params.channel_id))
|
||||||
|
}
|
||||||
|
|
||||||
return `/filters?${queryParams.toString()}`
|
return `/filters?${queryParams.toString()}`
|
||||||
},
|
},
|
||||||
@@ -22,6 +25,7 @@ export const filtersApi = baseApi.injectEndpoints({
|
|||||||
return {
|
return {
|
||||||
categories: response.data?.categories || [],
|
categories: response.data?.categories || [],
|
||||||
brands: response.data?.brands || [],
|
brands: response.data?.brands || [],
|
||||||
|
channels: response.data?.channels || [],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
keepUnusedDataFor: 300,
|
keepUnusedDataFor: 300,
|
||||||
@@ -40,7 +44,9 @@ export const filtersApi = baseApi.injectEndpoints({
|
|||||||
if (queryArgs.brand_id) {
|
if (queryArgs.brand_id) {
|
||||||
parts.push(`brd:${queryArgs.brand_id}`);
|
parts.push(`brd:${queryArgs.brand_id}`);
|
||||||
}
|
}
|
||||||
|
if (queryArgs.channel_id) {
|
||||||
|
parts.push(`chn:${queryArgs.channel_id}`);
|
||||||
|
}
|
||||||
return parts.length > 0 ? parts.join('|') : 'no-params';
|
return parts.length > 0 ? parts.join('|') : 'no-params';
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -66,7 +72,9 @@ export const filtersApi = baseApi.injectEndpoints({
|
|||||||
if (arg.brand_id) {
|
if (arg.brand_id) {
|
||||||
tags.push({ type: "Filters", id: `brd-${arg.brand_id}` });
|
tags.push({ type: "Filters", id: `brd-${arg.brand_id}` });
|
||||||
}
|
}
|
||||||
|
if (arg.channel_id) {
|
||||||
|
tags.push({ type: "Filters", id: `chn-${arg.channel_id}` });
|
||||||
|
}
|
||||||
return tags;
|
return tags;
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|||||||
11
src/app/api/flashSalesApi.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { baseApi } from "./baseApi";
|
||||||
|
|
||||||
|
export const flashSalesApi = baseApi.injectEndpoints({
|
||||||
|
endpoints: (builder) => ({
|
||||||
|
getFlashSales: builder.query({
|
||||||
|
query: () => "/flash-sales",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { useGetFlashSalesQuery } = flashSalesApi;
|
||||||
BIN
src/assets/arch_inter_side.jpg
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
src/assets/arch_street_side.jpg
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
src/assets/door.jpg
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
src/assets/engine.jpg
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
src/assets/floor.jpg
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
src/assets/hood.jpg
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
src/assets/maincar.webp
Normal file
|
After Width: | Height: | Size: 79 KiB |
BIN
src/assets/plastik_fender_liner_4.jpg
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
src/assets/roof.jpg
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
src/assets/trunk_flor.jpg
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
src/assets/trunk_lid.jpg
Normal file
|
After Width: | Height: | Size: 22 KiB |
109
src/components/Banner/Stories.module.scss
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
.storiesContainer {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1366px;
|
||||||
|
margin: 0 auto 24px;
|
||||||
|
padding: 0 0.75rem;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
padding: 0;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.storiesWrapper {
|
||||||
|
display: flex;
|
||||||
|
gap: 14px;
|
||||||
|
padding: 10px 4px;
|
||||||
|
overflow-x: auto;
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
scrollbar-width: none;
|
||||||
|
max-width: 1336px;
|
||||||
|
cursor: grab;
|
||||||
|
user-select: none;
|
||||||
|
&.dragging {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.storyButton {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
&:active .storyAvatar {
|
||||||
|
transform: scale(0.9);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Gradient ring — conic-gradient, padding trick, temiz */
|
||||||
|
.storyAvatar {
|
||||||
|
position: relative;
|
||||||
|
width: 68px;
|
||||||
|
height: 68px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: transform 0.15s ease;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: conic-gradient(#f44336, #e91e63, #ff9800, #f44336);
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
border-radius: 50%;
|
||||||
|
object-fit: cover;
|
||||||
|
background: #fff;
|
||||||
|
border: 2px solid #fff;
|
||||||
|
display: block;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.viewed::before {
|
||||||
|
background: #d0d0d0;
|
||||||
|
}
|
||||||
|
&.viewed img {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Badge/viewedIndicator tamamen kaldırıldı */
|
||||||
|
|
||||||
|
.storyLabel {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #333;
|
||||||
|
text-align: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 68px;
|
||||||
|
font-weight: 400;
|
||||||
|
transition: color 0.2s;
|
||||||
|
|
||||||
|
.storyButton:hover & {
|
||||||
|
color: #e91e63;
|
||||||
|
}
|
||||||
|
}
|
||||||
56
src/components/Banner/hook/useDragScroll.js
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { useRef, useEffect } from "react";
|
||||||
|
|
||||||
|
export function useDragScroll() {
|
||||||
|
const ref = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const el = ref.current;
|
||||||
|
if (!el) return;
|
||||||
|
|
||||||
|
let isDown = false;
|
||||||
|
let startX = 0;
|
||||||
|
let scrollLeft = 0;
|
||||||
|
|
||||||
|
const onMouseDown = (e) => {
|
||||||
|
isDown = true;
|
||||||
|
el.classList.add("dragging");
|
||||||
|
startX = e.pageX - el.offsetLeft;
|
||||||
|
scrollLeft = el.scrollLeft;
|
||||||
|
delete el.dataset.dragged;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMouseLeave = () => {
|
||||||
|
isDown = false;
|
||||||
|
el.classList.remove("dragging");
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMouseUp = () => {
|
||||||
|
isDown = false;
|
||||||
|
el.classList.remove("dragging");
|
||||||
|
setTimeout(() => delete el.dataset.dragged, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMouseMove = (e) => {
|
||||||
|
if (!isDown) return;
|
||||||
|
e.preventDefault();
|
||||||
|
const x = e.pageX - el.offsetLeft;
|
||||||
|
const walk = (x - startX) * 1.2;
|
||||||
|
if (Math.abs(walk) > 5) el.dataset.dragged = "true";
|
||||||
|
el.scrollLeft = scrollLeft - walk;
|
||||||
|
};
|
||||||
|
|
||||||
|
el.addEventListener("mousedown", onMouseDown);
|
||||||
|
el.addEventListener("mouseleave", onMouseLeave);
|
||||||
|
el.addEventListener("mouseup", onMouseUp);
|
||||||
|
el.addEventListener("mousemove", onMouseMove);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
el.removeEventListener("mousedown", onMouseDown);
|
||||||
|
el.removeEventListener("mouseleave", onMouseLeave);
|
||||||
|
el.removeEventListener("mouseup", onMouseUp);
|
||||||
|
el.removeEventListener("mousemove", onMouseMove);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return ref;
|
||||||
|
}
|
||||||
@@ -1,197 +1,152 @@
|
|||||||
import React, { useState, useEffect, useRef } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { Swiper, SwiperSlide } from "swiper/react";
|
import { Swiper, SwiperSlide } from "swiper/react";
|
||||||
import {
|
import { Autoplay, Thumbs, Pagination, Navigation, Mousewheel, FreeMode } from "swiper/modules";
|
||||||
Autoplay,
|
import { Skeleton } from "antd";
|
||||||
Thumbs,
|
|
||||||
Pagination,
|
|
||||||
Navigation,
|
|
||||||
Mousewheel,
|
|
||||||
FreeMode,
|
|
||||||
} from "swiper/modules";
|
|
||||||
import "swiper/css";
|
import "swiper/css";
|
||||||
import "swiper/css/pagination";
|
import "swiper/css/pagination";
|
||||||
import "swiper/css/thumbs";
|
import "swiper/css/thumbs";
|
||||||
import "swiper/css/navigation";
|
import "swiper/css/navigation";
|
||||||
|
|
||||||
import styles from "./Banner.module.scss";
|
import styles from "./Banner.module.scss";
|
||||||
import { useGetCarouselsQuery } from "../../app/api/bannersApi.js";
|
import storiesStyles from "./Stories.module.scss";
|
||||||
|
|
||||||
import { Skeleton } from "antd";
|
import { useGetCarouselsQuery, useGetStoriesQuery } from "../../app/api/bannersApi.js";
|
||||||
|
import StoryViewer from "../StoryViewer/StoryViewer";
|
||||||
|
import { useDragScroll } from "./hook/useDragScroll.js";
|
||||||
|
|
||||||
function Carousel() {
|
function Carousel() {
|
||||||
const { data, isLoading, isError } = useGetCarouselsQuery();
|
const { data: carouselData, isLoading, isError } = useGetCarouselsQuery();
|
||||||
|
const { data: storiesData, isLoading: isStoriesLoading, isError: isStoriesError } = useGetStoriesQuery();
|
||||||
|
|
||||||
const [thumbsSwiper, setThumbsSwiper] = useState(null);
|
const [thumbsSwiper, setThumbsSwiper] = useState(null);
|
||||||
const [activeIndex, setActiveIndex] = useState(0);
|
const [activeIndex, setActiveIndex] = useState(0);
|
||||||
const [isAnimating, setIsAnimating] = useState(true);
|
const [isAnimating, setIsAnimating] = useState(true);
|
||||||
const thumbSliderRef = useRef(null);
|
const [selectedStoryIndex, setSelectedStoryIndex] = useState(null);
|
||||||
|
const [viewedStoryIds, setViewedStoryIds] = useState(new Set());
|
||||||
|
|
||||||
|
const storiesScrollRef = useDragScroll();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsAnimating(false);
|
setIsAnimating(false);
|
||||||
setTimeout(() => setIsAnimating(true), 50);
|
const timer = setTimeout(() => setIsAnimating(true), 50);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
}, [activeIndex]);
|
}, [activeIndex]);
|
||||||
|
|
||||||
const updateScrollPosition = (targetIndex) => {
|
|
||||||
if (!thumbSliderRef.current) return;
|
|
||||||
|
|
||||||
const container = thumbSliderRef.current.querySelector(".swiper-wrapper");
|
|
||||||
const slideHeight = container.children[0]?.offsetHeight || 0;
|
|
||||||
const spaceBetween = 15;
|
|
||||||
const scrollPosition = targetIndex * (slideHeight + spaceBetween);
|
|
||||||
|
|
||||||
container.parentNode.scrollTop = scrollPosition;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSlideChange = (swiper) => {
|
const handleSlideChange = (swiper) => {
|
||||||
const newActiveIndex = swiper.realIndex;
|
setActiveIndex(swiper.realIndex);
|
||||||
setActiveIndex(newActiveIndex);
|
};
|
||||||
|
|
||||||
if (thumbsSwiper?.slides) {
|
const handleImageClick = (link) => {
|
||||||
const slidesPerView = 4;
|
if (link) window.open(link, "_blank", "noopener,noreferrer");
|
||||||
let targetIndex = newActiveIndex - Math.floor(slidesPerView / 2);
|
};
|
||||||
targetIndex = Math.max(
|
|
||||||
0,
|
|
||||||
Math.min(targetIndex, thumbsSwiper.slides.length - slidesPerView)
|
|
||||||
);
|
|
||||||
|
|
||||||
thumbsSwiper.slideTo(targetIndex, 300);
|
const handleStoryViewed = (storyIndex) => {
|
||||||
updateScrollPosition(targetIndex);
|
const storyId = storiesData?.data[storyIndex]?.id;
|
||||||
|
if (storyId) {
|
||||||
|
setViewedStoryIds((prev) => new Set(prev).add(storyId));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handler for clicking on carousel images
|
const handleStoryClick = (index) => {
|
||||||
const handleImageClick = (link) => {
|
if (storiesScrollRef.current?.dataset.dragged) return;
|
||||||
if (link) {
|
setSelectedStoryIndex(index);
|
||||||
window.open(link, '_blank', 'noopener,noreferrer');
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.carouselContainer}>
|
<div className={styles.carouselContainer}>
|
||||||
{/* Main slider skeleton */}
|
<div className={styles.mainSliderSkeleton}>
|
||||||
<div className={`${styles.mainSlider} skeleton-main-slider`}>
|
<Skeleton.Image active className={styles.fullWidthSkeleton} />
|
||||||
<Skeleton.Image active={true} className="main-skeleton-image" />
|
|
||||||
<style jsx>{`
|
|
||||||
.skeleton-main-slider {
|
|
||||||
width: 100%;
|
|
||||||
aspect-ratio: 16/9;
|
|
||||||
position: relative;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
border-radius: 8px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.main-skeleton-image {
|
|
||||||
width: 100% !important;
|
|
||||||
height: 100% !important;
|
|
||||||
}
|
|
||||||
`}</style>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Thumbnail Slider skeleton */}
|
|
||||||
<div className={`${styles.thumbSlider} skeleton-thumb-slider`}>
|
|
||||||
{[...Array(5)].map((_, index) => (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
className={`${styles.thumbWrapper} skeleton-thumb`}
|
|
||||||
>
|
|
||||||
<Skeleton.Image active={true} />
|
|
||||||
<style jsx>{`
|
|
||||||
.skeleton-thumb-slider {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
@media screen and (max-width:767px){
|
|
||||||
display: none;}
|
|
||||||
}
|
|
||||||
.skeleton-thumb {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
border-radius: 4px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
`}</style>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isError || !data || !data.data || data.data.length === 0) {
|
if (isError || !carouselData?.data?.length) return null;
|
||||||
return <div>No images available</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
{!isStoriesLoading && !isStoriesError && storiesData?.data?.length > 0 && (
|
||||||
|
<div className={storiesStyles.storiesContainer}>
|
||||||
|
<div className={storiesStyles.storiesWrapper} ref={storiesScrollRef}>
|
||||||
|
{storiesData.data.map((story, index) => {
|
||||||
|
const isViewed = viewedStoryIds.has(story.id);
|
||||||
|
return (
|
||||||
|
|
||||||
|
<button
|
||||||
|
key={story.id}
|
||||||
|
className={storiesStyles.storyButton}
|
||||||
|
onClick={() => handleStoryClick(index)}
|
||||||
|
>
|
||||||
|
<div className={`${storiesStyles.storyAvatar} ${isViewed ? storiesStyles.viewed : ""}`}>
|
||||||
|
<img src={story.thumbnail || story.photo} alt={story.title} />
|
||||||
|
</div>
|
||||||
|
<span className={storiesStyles.storyLabel}>{story.title}</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selectedStoryIndex !== null && (
|
||||||
|
<StoryViewer
|
||||||
|
stories={storiesData.data}
|
||||||
|
initialIndex={selectedStoryIndex}
|
||||||
|
onClose={() => setSelectedStoryIndex(null)}
|
||||||
|
onStoryViewed={handleStoryViewed}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className={styles.carouselContainer}>
|
<div className={styles.carouselContainer}>
|
||||||
{/* Main Slider */}
|
|
||||||
<Swiper
|
<Swiper
|
||||||
modules={[Thumbs, Pagination, Navigation, Autoplay]}
|
modules={[Thumbs, Pagination, Navigation, Autoplay]}
|
||||||
thumbs={{ swiper: thumbsSwiper }}
|
thumbs={{ swiper: thumbsSwiper && !thumbsSwiper.destroyed ? thumbsSwiper : null }}
|
||||||
autoplay={{ delay: 3000, disableOnInteraction: false }}
|
autoplay={{ delay: 3000, disableOnInteraction: false }}
|
||||||
loop={true}
|
loop={true}
|
||||||
pagination={{
|
pagination={{ clickable: true }}
|
||||||
clickable: true,
|
|
||||||
}}
|
|
||||||
navigation={true}
|
navigation={true}
|
||||||
className={styles.mainSlider}
|
className={styles.mainSlider}
|
||||||
onSlideChange={handleSlideChange}
|
onSlideChange={handleSlideChange}
|
||||||
>
|
>
|
||||||
{data.data.map((item) => (
|
{carouselData.data.map((item) => (
|
||||||
<SwiperSlide key={item.id}>
|
<SwiperSlide key={item.id}>
|
||||||
<div
|
<div
|
||||||
className={styles.imageWrapper}
|
className={styles.imageWrapper}
|
||||||
onClick={() => handleImageClick(item.link)}
|
onClick={() => handleImageClick(item.link)}
|
||||||
style={{ cursor: item.link ? 'pointer' : 'default' }}
|
style={{ cursor: item.link ? "pointer" : "default" }}
|
||||||
>
|
>
|
||||||
<img
|
<img src={item.image} alt={item.title || "Banner"} />
|
||||||
src={item.image}
|
|
||||||
alt={item.title || `Carousel Image ${item.id}`}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</SwiperSlide>
|
</SwiperSlide>
|
||||||
))}
|
))}
|
||||||
</Swiper>
|
</Swiper>
|
||||||
|
|
||||||
{/* Thumbnail Slider */}
|
|
||||||
<Swiper
|
<Swiper
|
||||||
ref={thumbSliderRef}
|
modules={[Thumbs, FreeMode, Mousewheel]}
|
||||||
modules={[Thumbs, Autoplay, FreeMode, Mousewheel]}
|
|
||||||
onSwiper={setThumbsSwiper}
|
onSwiper={setThumbsSwiper}
|
||||||
autoplay={{ delay: 3000 }}
|
|
||||||
slidesPerView={4}
|
slidesPerView={4}
|
||||||
spaceBetween={10}
|
spaceBetween={10}
|
||||||
direction="vertical"
|
direction="vertical"
|
||||||
watchSlidesProgress={true}
|
watchSlidesProgress={true}
|
||||||
slideToClickedSlide={true}
|
|
||||||
cssMode={true}
|
|
||||||
loop={false}
|
|
||||||
allowTouchMove={true}
|
|
||||||
className={styles.thumbSlider}
|
className={styles.thumbSlider}
|
||||||
>
|
>
|
||||||
{data.data.map((item, index) => (
|
{carouselData.data.map((item, index) => (
|
||||||
<SwiperSlide key={item.id}>
|
<SwiperSlide key={item.id}>
|
||||||
<div
|
<div className={`${styles.thumbWrapper} ${index === activeIndex ? styles.active : ""}`}>
|
||||||
className={`${styles.thumbWrapper} ${
|
<img src={item.thumbnail} alt={`Thumb ${index}`} />
|
||||||
index === activeIndex ? styles.active : ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src={item.thumbnail}
|
|
||||||
alt={item.title || `Thumbnail ${index + 1}`}
|
|
||||||
/>
|
|
||||||
{index === activeIndex && isAnimating && (
|
{index === activeIndex && isAnimating && (
|
||||||
<>
|
<div className={styles.progressContainer}>
|
||||||
<div className={styles.progressBarImg}></div>
|
<div className={styles.progressBarImg}></div>
|
||||||
<div className={styles.progressBar}></div>
|
<div className={styles.progressBar}></div>
|
||||||
</>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</SwiperSlide>
|
</SwiperSlide>
|
||||||
))}
|
))}
|
||||||
</Swiper>
|
</Swiper>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
212
src/components/CarConfigurator/Carconfigurator.jsx
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import styles from "./Carconfigurator.module.scss";
|
||||||
|
import maincar from "../../assets/maincar.webp";
|
||||||
|
import imgRoof from "../../assets/roof.jpg";
|
||||||
|
import imgHood from "../../assets/hood.jpg";
|
||||||
|
import imgFloor from "../../assets/floor.jpg";
|
||||||
|
import imgDoor from "../../assets/door.jpg";
|
||||||
|
import imgTrunkLid from "../../assets/trunk_lid.jpg";
|
||||||
|
import imgTrunkFloor from "../../assets/trunk_flor.jpg";
|
||||||
|
import imgEngine from "../../assets/engine.jpg";
|
||||||
|
import imgArchInter from "../../assets/arch_inter_side.jpg";
|
||||||
|
import imgArchStreet from "../../assets/arch_street_side.jpg";
|
||||||
|
import imgPlasticFender from "../../assets/plastik_fender_liner_4.jpg";
|
||||||
|
|
||||||
|
// ─── Zone image map ───────────────────────────────────────────────────────────
|
||||||
|
const ZONE_IMAGES = {
|
||||||
|
roof: imgRoof,
|
||||||
|
hood: imgHood,
|
||||||
|
floor: imgFloor,
|
||||||
|
doors: imgDoor,
|
||||||
|
trunk_lid: imgTrunkLid,
|
||||||
|
trunk_floor: imgTrunkFloor,
|
||||||
|
engine: imgEngine,
|
||||||
|
arch_interior: imgArchInter,
|
||||||
|
arch_street: imgArchStreet,
|
||||||
|
fender_liner: imgPlasticFender,
|
||||||
|
wheel: imgArchStreet,
|
||||||
|
};
|
||||||
|
|
||||||
|
const DOTS = [
|
||||||
|
{ id: "roof", x: 44, y: 22 },
|
||||||
|
{ id: "trunk_lid", x: 80, y: 27 },
|
||||||
|
{ id: "trunk_floor", x: 68, y: 46 },
|
||||||
|
{ id: "arch_interior", x: 75, y: 43 },
|
||||||
|
{ id: "fender_liner", x: 73, y: 51 },
|
||||||
|
{ id: "arch_street", x: 47, y: 69 },
|
||||||
|
{ id: "engine", x: 40, y: 59 },
|
||||||
|
{ id: "floor", x: 58, y: 61 },
|
||||||
|
{ id: "doors", x: 74, y: 80 },
|
||||||
|
{ id: "hood", x: 17, y: 52 },
|
||||||
|
{ id: "wheel", x: 42, y: 82 },
|
||||||
|
];
|
||||||
|
|
||||||
|
// ─── Product List ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function ProductList({ zone, pkg, bodyType, appData }) {
|
||||||
|
if (!appData) return <p className={styles.emptyMsg}>Загрузка...</p>;
|
||||||
|
|
||||||
|
const zoneInfo = appData.zones[zone];
|
||||||
|
const products = zoneInfo?.products?.[bodyType]?.[pkg] || [];
|
||||||
|
const total = products.reduce((s, p) => s + p.price * p.qty, 0);
|
||||||
|
const zoneImage = ZONE_IMAGES[zone];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.productList}>
|
||||||
|
{zoneImage ? (
|
||||||
|
<div className={styles.zoneImageWrapper}>
|
||||||
|
<img src={zoneImage} alt={zoneInfo?.label} className={styles.zoneImage} />
|
||||||
|
<span className={styles.zoneImageLabel}>{zoneInfo?.label}</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<h3 className={styles.zoneTitle}>{zoneInfo?.label}</h3>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{products.length === 0 && (
|
||||||
|
<p className={styles.emptyMsg}>Нет товаров для данной зоны / пакета.</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{products.map((p, i) => (
|
||||||
|
<div key={i} className={styles.productRow}>
|
||||||
|
<span className={styles.productName}>{p.name}</span>
|
||||||
|
<span className={styles.productCalc}>
|
||||||
|
<b>{p.price.toLocaleString("ru")} m</b>
|
||||||
|
{" × "}
|
||||||
|
{p.qty} {p.unit}
|
||||||
|
{" = "}
|
||||||
|
<b className={styles.subtotal}>
|
||||||
|
{(p.price * p.qty).toLocaleString("ru")} m
|
||||||
|
</b>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{products.length > 0 && (
|
||||||
|
<div className={styles.totalRow}>
|
||||||
|
Итого: <strong>{total.toLocaleString("ru")} m</strong>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* <div className={styles.actionButtons}>
|
||||||
|
<button className={styles.btnBuy}>Купить</button>
|
||||||
|
<button className={styles.btnView}>Смотреть</button>
|
||||||
|
</div> */}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Main Component ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export default function CarConfigurator() {
|
||||||
|
const [appData, setAppData] = useState(null);
|
||||||
|
const [selectedBody, setSelectedBody]= useState(null);
|
||||||
|
const [selectedPkg, setSelectedPkg] = useState(null);
|
||||||
|
const [activeZone, setActiveZone] = useState("roof");
|
||||||
|
const [tooltip, setTooltip] = useState(null);
|
||||||
|
|
||||||
|
// Load data.json — served from /public/data.json, readable by all users
|
||||||
|
useEffect(() => {
|
||||||
|
fetch("/frontend-api/data")
|
||||||
|
.then((r) => r.json())
|
||||||
|
.then((data) => {
|
||||||
|
setAppData(data);
|
||||||
|
setSelectedBody(data.bodyTypes[0]?.id || "sedan");
|
||||||
|
setSelectedPkg(data.packages[0] || "Максимум");
|
||||||
|
})
|
||||||
|
.catch(console.error);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!appData) {
|
||||||
|
return (
|
||||||
|
<div className={styles.wrapper}>
|
||||||
|
<p className={styles.emptyMsg}>Загрузка данных...</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.wrapper}>
|
||||||
|
|
||||||
|
{/* ── Body Type Bar — TOP ──────────────────────────────── */}
|
||||||
|
<div className={styles.bodyBar}>
|
||||||
|
<p className={styles.sectionLabel}>Тип кузова</p>
|
||||||
|
<div className={styles.bodyGrid}>
|
||||||
|
{appData.bodyTypes.map((b) => (
|
||||||
|
<button
|
||||||
|
key={b.id}
|
||||||
|
className={`${styles.bodyCard} ${selectedBody === b.id ? styles.active : ""}`}
|
||||||
|
onClick={() => setSelectedBody(b.id)}
|
||||||
|
>
|
||||||
|
<span className={styles.bodyIcon}>
|
||||||
|
{b.image ? <img src={b.image} alt={b.label} /> : b.icon}
|
||||||
|
</span>
|
||||||
|
<span className={styles.bodyLabel}>{b.label}</span>
|
||||||
|
{selectedBody === b.id && <span className={styles.activeDot} />}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ── Main ─────────────────────────────────────────────── */}
|
||||||
|
<div className={styles.main}>
|
||||||
|
|
||||||
|
{/* Car visual */}
|
||||||
|
<div className={styles.carWrapper}>
|
||||||
|
<img
|
||||||
|
className={styles.carImage}
|
||||||
|
src={maincar}
|
||||||
|
alt="Автомобиль"
|
||||||
|
draggable={false}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{DOTS.map((dot) => (
|
||||||
|
<button
|
||||||
|
key={dot.id}
|
||||||
|
className={`${styles.dot} ${activeZone === dot.id ? styles.dotActive : ""}`}
|
||||||
|
style={{ left: `${dot.x}%`, top: `${dot.y}%` }}
|
||||||
|
onClick={() => setActiveZone(dot.id)}
|
||||||
|
onMouseEnter={() => setTooltip(dot)}
|
||||||
|
onMouseLeave={() => setTooltip(null)}
|
||||||
|
aria-label={appData.zones[dot.id]?.label}
|
||||||
|
>
|
||||||
|
<span className={styles.dotRipple} />
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{tooltip && (
|
||||||
|
<div
|
||||||
|
className={styles.tooltip}
|
||||||
|
style={{ left: `${tooltip.x}%`, top: `${tooltip.y - 8}%` }}
|
||||||
|
>
|
||||||
|
{appData.zones[tooltip.id]?.label ?? tooltip.id}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right panel */}
|
||||||
|
<div className={styles.panel}>
|
||||||
|
<div className={styles.pkgTabs}>
|
||||||
|
{appData.packages.map((p) => (
|
||||||
|
<button
|
||||||
|
key={p}
|
||||||
|
className={`${styles.pkgTab} ${selectedPkg === p ? styles.pkgActive : ""}`}
|
||||||
|
onClick={() => setSelectedPkg(p)}
|
||||||
|
>
|
||||||
|
{p}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.productGridContainer}>
|
||||||
|
<ProductList
|
||||||
|
zone={activeZone}
|
||||||
|
pkg={selectedPkg}
|
||||||
|
bodyType={selectedBody}
|
||||||
|
appData={appData}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
397
src/components/CarConfigurator/Carconfigurator.module.scss
Normal file
@@ -0,0 +1,397 @@
|
|||||||
|
// ── Variables ─────────────────────────────────────────────────
|
||||||
|
$orange: #f26522;
|
||||||
|
$orange-dark: #d4551a;
|
||||||
|
$bg: #f5f5f5;
|
||||||
|
$white: #ffffff;
|
||||||
|
$border: #e0e0e0;
|
||||||
|
$text: #1a1a1a;
|
||||||
|
$muted: #888;
|
||||||
|
$radius-sm: 6px;
|
||||||
|
$radius-md: 10px;
|
||||||
|
$radius-lg: 16px;
|
||||||
|
$transition: 0.2s ease;
|
||||||
|
|
||||||
|
// ── Wrapper ───────────────────────────────────────────────────
|
||||||
|
.wrapper {
|
||||||
|
font-family: "Segoe UI", sans-serif;
|
||||||
|
background: $bg;
|
||||||
|
border-radius: $radius-lg;
|
||||||
|
max-width: 1336px;
|
||||||
|
margin: 0 auto;
|
||||||
|
color: $text;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
padding-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Body Bar (TOP) ────────────────────────────────────────────
|
||||||
|
.bodyBar {
|
||||||
|
background: $white;
|
||||||
|
border-radius: $radius-lg;
|
||||||
|
border: 1px solid $border;
|
||||||
|
padding: 14px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionLabel {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
color: $muted;
|
||||||
|
margin: 0 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Main Layout ───────────────────────────────────────────────
|
||||||
|
.main {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 380px;
|
||||||
|
gap: 20px;
|
||||||
|
align-items: start;
|
||||||
|
|
||||||
|
@media (max-width: 860px) {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Car Wrapper ───────────────────────────────────────────────
|
||||||
|
.carWrapper {
|
||||||
|
position: relative;
|
||||||
|
background: $white;
|
||||||
|
border-radius: $radius-lg;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid $border;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carImage {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
display: block;
|
||||||
|
user-select: none;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Dots ──────────────────────────────────────────────────────
|
||||||
|
.dot {
|
||||||
|
position: absolute;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: $orange;
|
||||||
|
border: 3px solid $white;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
|
||||||
|
transition: transform $transition, background $transition;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translate(-50%, -50%) scale(1.25);
|
||||||
|
background: $orange-dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.dotActive {
|
||||||
|
background: $white;
|
||||||
|
border-color: $orange;
|
||||||
|
box-shadow: 0 0 0 3px $orange;
|
||||||
|
|
||||||
|
.dotRipple {
|
||||||
|
animation: ripple 1.2s ease-out infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dotRipple {
|
||||||
|
position: absolute;
|
||||||
|
inset: -6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid $orange;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes ripple {
|
||||||
|
0% { inset: -4px; opacity: 0.8; }
|
||||||
|
100% { inset: -16px; opacity: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Tooltip ───────────────────────────────────────────────────
|
||||||
|
.tooltip {
|
||||||
|
position: absolute;
|
||||||
|
transform: translate(-50%, -100%);
|
||||||
|
background: rgba(0, 0, 0, 0.78);
|
||||||
|
color: $white;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
white-space: nowrap;
|
||||||
|
pointer-events: none;
|
||||||
|
margin-top: -8px;
|
||||||
|
z-index: 10;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
border: 5px solid transparent;
|
||||||
|
border-top-color: rgba(0, 0, 0, 0.78);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Right Panel ───────────────────────────────────────────────
|
||||||
|
.panel {
|
||||||
|
background: $white;
|
||||||
|
border-radius: $radius-lg;
|
||||||
|
border: 1px solid $border;
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
max-height: 90vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Body Grid ─────────────────────────────────────────────────
|
||||||
|
.bodyGrid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(9, 1fr);
|
||||||
|
gap: 6px;
|
||||||
|
|
||||||
|
@media (max-width: 860px) {
|
||||||
|
grid-template-columns: repeat(5, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bodyCard {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 8px 4px;
|
||||||
|
background: $bg;
|
||||||
|
border: 2px solid $border;
|
||||||
|
border-radius: $radius-md;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color $transition, box-shadow $transition;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: $orange;
|
||||||
|
box-shadow: 0 2px 8px rgba($orange, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
border-color: $orange;
|
||||||
|
background: lighten($orange, 45%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bodyIcon {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bodyIcon {
|
||||||
|
max-height: 60px;
|
||||||
|
max-width: 60px;
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
.productGridContainer {
|
||||||
|
max-height: 400px; /* Yüksekliği buradan ayarlayabilirsiniz */
|
||||||
|
overflow-y: auto;
|
||||||
|
padding-right: 10px; /* Kaydırma çubuğu için boşluk */
|
||||||
|
}
|
||||||
|
|
||||||
|
.bodyLabel {
|
||||||
|
font-size: 0.6rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-align: center;
|
||||||
|
color: $text;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activeDot {
|
||||||
|
position: absolute;
|
||||||
|
top: 5px;
|
||||||
|
right: 5px;
|
||||||
|
width: 7px;
|
||||||
|
height: 7px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: $orange;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Package Tabs ──────────────────────────────────────────────
|
||||||
|
.pkgTabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
border-bottom: 2px solid $border;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pkgTab {
|
||||||
|
flex: 1;
|
||||||
|
padding: 6px 4px;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 700;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
color: $muted;
|
||||||
|
cursor: pointer;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
margin-bottom: -14px;
|
||||||
|
transition: color $transition, border-color $transition;
|
||||||
|
|
||||||
|
&:hover { color: $orange; }
|
||||||
|
|
||||||
|
&.pkgActive {
|
||||||
|
color: $orange;
|
||||||
|
border-bottom-color: $orange;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Zone Image ────────────────────────────────────────────────
|
||||||
|
.zoneImageWrapper {
|
||||||
|
position: relative;
|
||||||
|
border-radius: $radius-md;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 140px;
|
||||||
|
background: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zoneImage {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
display: block;
|
||||||
|
opacity: 0.9;
|
||||||
|
transition: opacity $transition;
|
||||||
|
|
||||||
|
&:hover { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.zoneImageLabel {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
padding: 6px 10px;
|
||||||
|
background: linear-gradient(transparent, rgba(0, 0, 0, 0.65));
|
||||||
|
color: $white;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Product List ──────────────────────────────────────────────
|
||||||
|
.productList {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zoneTitle {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 0 0 4px;
|
||||||
|
color: $text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emptyMsg {
|
||||||
|
font-size: 0.78rem;
|
||||||
|
color: $muted;
|
||||||
|
font-style: italic;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.productRow {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
background: $bg;
|
||||||
|
border-radius: $radius-sm;
|
||||||
|
border-left: 3px solid $orange;
|
||||||
|
}
|
||||||
|
|
||||||
|
.productName {
|
||||||
|
font-size: 0.78rem;
|
||||||
|
color: $text;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.productCalc {
|
||||||
|
font-size: 0.74rem;
|
||||||
|
color: $muted;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtotal {
|
||||||
|
color: $orange-dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
.totalRow {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: $text;
|
||||||
|
padding-top: 4px;
|
||||||
|
border-top: 1px solid $border;
|
||||||
|
text-align: right;
|
||||||
|
|
||||||
|
strong {
|
||||||
|
color: $orange;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Action Buttons ────────────────────────────────────────────
|
||||||
|
.actionButtons {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnBuy {
|
||||||
|
flex: 1;
|
||||||
|
padding: 10px;
|
||||||
|
background: $orange;
|
||||||
|
color: $white;
|
||||||
|
border: none;
|
||||||
|
border-radius: $radius-sm;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background $transition;
|
||||||
|
|
||||||
|
&:hover { background: $orange-dark; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnView {
|
||||||
|
flex: 1;
|
||||||
|
padding: 10px;
|
||||||
|
background: $white;
|
||||||
|
color: $orange;
|
||||||
|
border: 2px solid $orange;
|
||||||
|
border-radius: $radius-sm;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background $transition, color $transition;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: $orange;
|
||||||
|
color: $white;
|
||||||
|
}
|
||||||
|
}
|
||||||
80
src/components/CategoryCarousel/CategoryCarousel.jsx
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { Swiper, SwiperSlide } from "swiper/react";
|
||||||
|
import { Navigation, FreeMode } from "swiper/modules";
|
||||||
|
import { useGetCategoriesQuery } from "../../app/api/categories.js";
|
||||||
|
import styles from "./CategorySlider.module.scss";
|
||||||
|
|
||||||
|
import "swiper/css";
|
||||||
|
import "swiper/css/navigation";
|
||||||
|
import "swiper/css/free-mode";
|
||||||
|
|
||||||
|
const CategoryCarousel = () => {
|
||||||
|
const { data, isLoading } = useGetCategoriesQuery("tree");
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const mainCategories =
|
||||||
|
data?.data?.filter((cat) => cat.parent_id === null) ?? [];
|
||||||
|
|
||||||
|
const handleCardClick = (id) => {
|
||||||
|
navigate(`/category/${id}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoading || mainCategories.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.wrapper}>
|
||||||
|
<Swiper
|
||||||
|
modules={[Navigation, FreeMode]}
|
||||||
|
navigation={{
|
||||||
|
prevEl: `.${styles.prev}`,
|
||||||
|
nextEl: `.${styles.next}`,
|
||||||
|
}}
|
||||||
|
freeMode={{ enabled: true, momentum: true, momentumRatio: 0.5 }}
|
||||||
|
slidesPerView="auto"
|
||||||
|
spaceBetween={12}
|
||||||
|
grabCursor={true}
|
||||||
|
className={styles.swiper}
|
||||||
|
>
|
||||||
|
{mainCategories.map((cat) => {
|
||||||
|
const thumb =
|
||||||
|
cat.media?.[0]?.images_400x400 ??
|
||||||
|
cat.media?.[0]?.thumbnail ??
|
||||||
|
null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SwiperSlide key={cat.id} className={styles.slide}>
|
||||||
|
<div
|
||||||
|
className={styles.card}
|
||||||
|
onClick={() => handleCardClick(cat.id)}
|
||||||
|
>
|
||||||
|
<div className={styles.imgWrap}>
|
||||||
|
{thumb ? (
|
||||||
|
<img
|
||||||
|
src={thumb}
|
||||||
|
alt={cat.name}
|
||||||
|
loading="lazy"
|
||||||
|
draggable={false}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className={styles.placeholder} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<span className={styles.name}>{cat.name}</span>
|
||||||
|
</div>
|
||||||
|
</SwiperSlide>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Swiper>
|
||||||
|
|
||||||
|
<button className={`${styles.arrow} ${styles.prev}`} aria-label="Scroll left">
|
||||||
|
‹
|
||||||
|
</button>
|
||||||
|
<button className={`${styles.arrow} ${styles.next}`} aria-label="Scroll right">
|
||||||
|
›
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CategoryCarousel;
|
||||||
151
src/components/CategoryCarousel/CategorySlider.module.scss
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
.wrapper {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
margin: 20px 0 24px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Swiper overrides ── */
|
||||||
|
.swiper {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 4px 2px 8px 2px !important;
|
||||||
|
|
||||||
|
:global(.swiper-button-disabled) {
|
||||||
|
opacity: 0.3;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide {
|
||||||
|
width: 200px !important;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Card ── */
|
||||||
|
.card {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #f5f5f5;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
transition: box-shadow 0.2s ease, transform 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.1);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
border: 1px solid #d24141;
|
||||||
|
|
||||||
|
.imgWrap img {
|
||||||
|
transform: scale(1.04);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Image area ── */
|
||||||
|
.imgWrap {
|
||||||
|
width: 100%;
|
||||||
|
// aspect-ratio: 4 / 3;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #ebebeb;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(135deg, #e0e0e0 0%, #d0d0d0 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Name label — sabit yükseklik ── */
|
||||||
|
.name {
|
||||||
|
padding: 10px 12px 12px 12px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #222;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 1.4;
|
||||||
|
background: #fff;
|
||||||
|
/* 2 satır = 2 × 13px × 1.4 + padding = ~60px — tüm kartlar eşit */
|
||||||
|
min-height: 60px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Arrow buttons ── */
|
||||||
|
.arrow {
|
||||||
|
position: absolute;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
background: #fff;
|
||||||
|
font-size: 22px;
|
||||||
|
line-height: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #444;
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
|
||||||
|
transition: background 0.15s, border-color 0.15s, color 0.15s, box-shadow 0.15s;
|
||||||
|
padding: 0;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #d24141;
|
||||||
|
border-color: #d24141;
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: 0 2px 8px rgba(210, 65, 65, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.prev {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.next {
|
||||||
|
margin-left: 4px;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Responsive ── */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.arrow {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide {
|
||||||
|
width: 140px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.slide {
|
||||||
|
width: 120px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 8px 8px 10px 8px;
|
||||||
|
min-height: 52px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,77 +1,116 @@
|
|||||||
|
// DropdownMenu.module.scss
|
||||||
|
|
||||||
.dropdownContainer {
|
.dropdownContainer {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
@media screen and (max-width: 1023px) {
|
@media screen and (max-width: 1023px) {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- TRIGGER BUTTON ----
|
||||||
.navButton {
|
.navButton {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 5px;
|
|
||||||
border: none;
|
|
||||||
padding-top: 0.25rem;
|
|
||||||
padding-bottom: 0.25rem;
|
|
||||||
padding-left: 0.875rem;
|
|
||||||
padding-right: 0.875rem;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
border: none;
|
||||||
|
padding: 0.25rem 0.875rem;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
height: 2.5rem;
|
height: 2.5rem;
|
||||||
font-size: 0.875rem;
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
color: #4b5563;
|
color: #4b5563;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
transition: background-color 0.15s, color 0.15s;
|
||||||
|
position: relative;
|
||||||
|
z-index: 999;
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: #f3f4f6;
|
background-color: #f3f4f6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.navButtonActive {
|
||||||
|
background-color: #e63946;
|
||||||
|
color: #ffffff;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- OVERLAY ----
|
||||||
|
.overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.45);
|
||||||
|
z-index: 998;
|
||||||
|
animation: fadeIn 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- WRAPPER + ANIMATION ----
|
||||||
.dropdownWrapper {
|
.dropdownWrapper {
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdownPanel {
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 100%;
|
top: calc(100% + 8px);
|
||||||
margin-top: 8px;
|
left: 0;
|
||||||
z-index: 50;
|
z-index: 999;
|
||||||
|
animation: slideDown 0.18s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideDown {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-6px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- PANEL SHELL ----
|
||||||
|
.dropdownPanel {
|
||||||
display: flex;
|
display: flex;
|
||||||
background: white;
|
background: #fff;
|
||||||
border: 1px solid #e5e7eb;
|
border: 1px solid #e5e7eb;
|
||||||
border-radius: 6px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||||
box-sizing: border-box;
|
overflow: hidden;
|
||||||
width: 1366px;
|
width: 1336px;
|
||||||
padding: 0 1.375rem;
|
max-height: 520px;
|
||||||
|
max-width: calc(100vw - 32px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- LEFT LIST ----
|
||||||
.categoriesList {
|
.categoriesList {
|
||||||
flex: 1;
|
width: 270px;
|
||||||
max-height: 500px;
|
flex-shrink: 0;
|
||||||
|
border-right: 1px solid #e5e7eb;
|
||||||
|
padding: 10px 0;
|
||||||
|
max-height: 520px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
border-right: 1px solid #ebe7eb;
|
|
||||||
padding: 20px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 5px;
|
|
||||||
|
|
||||||
// &::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
// width: 6px;
|
width: 6px;
|
||||||
// }
|
}
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
&::-webkit-scrollbar-track {
|
background: #f9fafb;
|
||||||
background: #e5e7eb;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&::-webkit-scrollbar-thumb {
|
&::-webkit-scrollbar-thumb {
|
||||||
background: #d1d5db;
|
background: #d1d5db;
|
||||||
}
|
border-radius: 10px;
|
||||||
.title {
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: #888888;
|
background: #9ca3af;
|
||||||
}
|
|
||||||
&:active {
|
|
||||||
color: #888888;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -79,156 +118,169 @@
|
|||||||
.categoryItem {
|
.categoryItem {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
justify-content: space-between;
|
||||||
padding: 6px;
|
padding: 9px 16px;
|
||||||
|
font-size: 16px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background-color 0.2s;
|
color: #000;
|
||||||
border: 1px solid #3615371a;
|
transition: background-color 0.12s, color 0.12s;
|
||||||
border-radius: 6px;
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: #f9fafb;
|
background-color: #f3f4f6;
|
||||||
|
color: #e63946;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
background-color: #f3f4f6;
|
background-color: #f3f4f6;
|
||||||
}
|
color: #e63946;
|
||||||
|
|
||||||
.icon {
|
.title {
|
||||||
font-size: 14px;
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
&:hover {
|
|
||||||
color: #888888;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chevron {
|
||||||
|
color: #9ca3af;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- RIGHT CONTENT PANEL ----
|
||||||
.contentPanel {
|
.contentPanel {
|
||||||
flex: 3;
|
flex: 1;
|
||||||
padding: 16px;
|
padding: 20px 24px;
|
||||||
max-height: 400px;
|
max-height: 520px;
|
||||||
overflow-y: hidden;
|
overflow-y: auto;
|
||||||
|
background: #ffffff;
|
||||||
|
|
||||||
// &::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
// width: 6px;
|
width: 6px;
|
||||||
// }
|
}
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
&::-webkit-scrollbar-track {
|
background: #f9fafb;
|
||||||
background: #e5e7eb;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&::-webkit-scrollbar-thumb {
|
&::-webkit-scrollbar-thumb {
|
||||||
background: #d1d5db;
|
background: #d1d5db;
|
||||||
// border-radius: 3px;
|
border-radius: 10px;
|
||||||
}
|
|
||||||
.title {
|
|
||||||
cursor: pointer;
|
|
||||||
color: #361517;
|
|
||||||
font-size: 24px;
|
|
||||||
font-weight: 600;
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: #888888;
|
background: #9ca3af;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.column {
|
|
||||||
display: flex;
|
.panelTitle {
|
||||||
flex-direction: column;
|
font-size: 20px;
|
||||||
flex: 2;
|
font-weight: 700;
|
||||||
text-align: left;
|
color: #111827;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #e63946;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// COLUMN GRID MODE
|
||||||
|
// SONRA — column layout (iyi, masonry gibi akar)
|
||||||
|
.columnsGrid {
|
||||||
|
columns: 250px auto;
|
||||||
|
column-gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.columnSection {
|
||||||
|
break-inside: avoid;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
display: inline-block; // break-inside'ın çalışması için zorunlu
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sectionTitle {
|
.sectionTitle {
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 500;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
color: #361517;
|
|
||||||
cursor: pointer;
|
|
||||||
&:hover {
|
|
||||||
color: #888888;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.subcategoryList {
|
|
||||||
margin-bottom: 24px;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subcategoryItem {
|
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #361517;
|
font-weight: 800;
|
||||||
padding: 4px 0;
|
color: #111827;
|
||||||
|
margin-bottom: 6px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: color 0.2s;
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: #888888;
|
color: #e63946;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.subCategoriesContainer {
|
.leafItem {
|
||||||
display: flex;
|
display: block;
|
||||||
flex-direction: column;
|
font-size: 14px;
|
||||||
max-height: 360px;
|
color: #4b5563;
|
||||||
overflow-y: auto;
|
padding: 3px 0;
|
||||||
}
|
|
||||||
|
|
||||||
.nestedCategoryContainer:last-child {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nestedCategoryContainer {
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nestedCategoryItem {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 8px 12px;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: color 0.12s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #e63946;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FLAT LIST MODE
|
||||||
|
.flatList {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px 12px;
|
||||||
|
align-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flatListBordered {
|
||||||
|
margin-top: 16px;
|
||||||
|
padding-top: 16px;
|
||||||
|
border-top: 1px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flatItem {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #4b5563;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
transition: background-color 0.12s, color 0.12s;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: #f3f4f6;
|
background-color: #f3f4f6;
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.categoryLabel {
|
.navButtonLoading {
|
||||||
flex: 1;
|
opacity: 0.7;
|
||||||
|
cursor: wait;
|
||||||
|
|
||||||
|
.categoryIcon {
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.loadingDots {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
gap: 3px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
span {
|
||||||
font-size: 14px;
|
width: 4px;
|
||||||
|
height: 4px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: currentColor;
|
||||||
|
animation: dotPulse 1.2s infinite ease-in-out;
|
||||||
|
|
||||||
|
&:nth-child(2) { animation-delay: 0.2s; }
|
||||||
|
&:nth-child(3) { animation-delay: 0.4s; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.expandButton,
|
@keyframes dotPulse {
|
||||||
.navigateButton {
|
0%, 80%, 100% { transform: scale(0.6); opacity: 0.4; }
|
||||||
display: flex;
|
40% { transform: scale(1); opacity: 1; }
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
border-radius: 4px;
|
|
||||||
background-color: transparent;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: #e5e7eb;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.nestedChildren {
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.noSubcategories {
|
|
||||||
color: #6b7280;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
}
|
||||||
@@ -1,83 +1,62 @@
|
|||||||
|
// DropdownMenu.jsx
|
||||||
import React, { useState, useEffect, useRef } from "react";
|
import React, { useState, useEffect, useRef } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { ChevronRight } from "lucide-react";
|
||||||
import styles from "./DropdownMenu.module.scss";
|
import styles from "./DropdownMenu.module.scss";
|
||||||
import { useGetCategoriesQuery } from "../../app/api/categories";
|
import { useGetCategoriesQuery } from "../../app/api/categories";
|
||||||
import { CategoryIcon } from "../Icons";
|
import { CategoryIcon } from "../Icons";
|
||||||
import { ChevronRight, ChevronDown } from "lucide-react"; // Assuming you have access to lucide-react or similar
|
|
||||||
|
|
||||||
const NestedCategory = ({
|
const ContentPanel = ({ category, onSelect, onClose }) => {
|
||||||
category,
|
if (!category) return null;
|
||||||
level = 0,
|
|
||||||
handleCategorySelect,
|
|
||||||
closeDropdown,
|
|
||||||
}) => {
|
|
||||||
const [isExpanded, setIsExpanded] = useState(false);
|
|
||||||
const hasChildren = category.children && category.children.length > 0;
|
|
||||||
|
|
||||||
const handleClick = (e) => {
|
const children = category.children || [];
|
||||||
e.stopPropagation();
|
const withChildren = children.filter((c) => c.children?.length > 0);
|
||||||
if (hasChildren) {
|
const withoutChildren = children.filter((c) => !c.children?.length);
|
||||||
setIsExpanded(!isExpanded);
|
|
||||||
} else {
|
|
||||||
handleCategorySelect(category);
|
|
||||||
closeDropdown();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDirectNavigation = (e) => {
|
const allColumns = [
|
||||||
e.stopPropagation();
|
...withChildren,
|
||||||
handleCategorySelect(category);
|
...withoutChildren.map((c) => ({ ...c, children: [] })),
|
||||||
closeDropdown();
|
];
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={styles.contentPanel}>
|
||||||
className={styles.nestedCategoryContainer}
|
<h2
|
||||||
style={{ paddingLeft: `${level * 16}px` }}
|
className={styles.panelTitle}
|
||||||
>
|
onClick={() => {
|
||||||
<div className={styles.nestedCategoryItem} onClick={handleClick}>
|
onSelect(category);
|
||||||
<div className={styles.categoryLabel}>
|
onClose();
|
||||||
<span className={styles.title}>{category.name}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{hasChildren && (
|
|
||||||
<button
|
|
||||||
className={styles.expandButton}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setIsExpanded(!isExpanded);
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isExpanded ? (
|
{category.name}
|
||||||
<ChevronDown size={16} />
|
</h2>
|
||||||
) : (
|
|
||||||
<ChevronRight size={16} />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{hasChildren && (
|
{allColumns.length > 0 && (
|
||||||
<button
|
<div className={styles.columnsGrid}>
|
||||||
className={styles.navigateButton}
|
{allColumns.map((sub) => (
|
||||||
onClick={handleDirectNavigation}
|
<div key={sub.id} className={styles.columnSection}>
|
||||||
title="Go to category"
|
<div
|
||||||
|
className={styles.sectionTitle}
|
||||||
|
onClick={() => {
|
||||||
|
onSelect(sub);
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
→
|
{sub.name}
|
||||||
</button>
|
</div>
|
||||||
)}
|
{sub.children?.map((leaf) => (
|
||||||
|
<span
|
||||||
|
key={leaf.id}
|
||||||
|
className={styles.leafItem}
|
||||||
|
onClick={() => {
|
||||||
|
onSelect(leaf);
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{leaf.name}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{hasChildren && isExpanded && (
|
|
||||||
<div className={styles.nestedChildren}>
|
|
||||||
{category.children.map((child) => (
|
|
||||||
<NestedCategory
|
|
||||||
key={child.id}
|
|
||||||
category={child}
|
|
||||||
level={level + 1}
|
|
||||||
handleCategorySelect={handleCategorySelect}
|
|
||||||
closeDropdown={closeDropdown}
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -89,113 +68,85 @@ const DropdownMenu = () => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const dropdownRef = useRef(null);
|
const dropdownRef = useRef(null);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: categoriesData,
|
data: categoriesData,
|
||||||
isLoading,
|
isLoading,
|
||||||
error,
|
error,
|
||||||
} = useGetCategoriesQuery("tree");
|
} = useGetCategoriesQuery("tree");
|
||||||
|
|
||||||
const categories = categoriesData?.data || [];
|
const categories = categoriesData?.data || [];
|
||||||
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [activeMainCategory, setActiveMainCategory] = useState(null);
|
const [activeCategory, setActiveCategory] = useState(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (categories.length > 0) {
|
if (categories.length > 0 && !activeCategory) {
|
||||||
const defaultCategory =
|
setActiveCategory(categories[0]);
|
||||||
categories.find((cat) => cat.name === "Aýallar üçin") || categories[0];
|
|
||||||
setActiveMainCategory(defaultCategory);
|
|
||||||
}
|
}
|
||||||
}, [categories]);
|
}, [categories]);
|
||||||
|
|
||||||
const handleToggle = () => {
|
useEffect(() => {
|
||||||
setIsOpen(!isOpen);
|
const handler = (e) => {
|
||||||
};
|
if (dropdownRef.current && !dropdownRef.current.contains(e.target)) {
|
||||||
|
setIsOpen(false);
|
||||||
const handleMouseLeave = () => {
|
|
||||||
if (categories.length > 0) {
|
|
||||||
const defaultCategory =
|
|
||||||
categories.find((cat) => cat.name === "Aýallar üçin") || categories[0];
|
|
||||||
setActiveMainCategory(defaultCategory);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
document.addEventListener("mousedown", handler);
|
||||||
|
return () => document.removeEventListener("mousedown", handler);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleCategorySelect = (category) => {
|
const handleSelect = (category) => {
|
||||||
navigate(`/category/${category.id}`, { state: { category } });
|
navigate(`/category/${category.id}`, { state: { category } });
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Close dropdown when clicking outside
|
|
||||||
useEffect(() => {
|
|
||||||
const handleClickOutside = (event) => {
|
|
||||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
|
|
||||||
setIsOpen(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener("mousedown", handleClickOutside);
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener("mousedown", handleClickOutside);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (isLoading) return <div>Loading...</div>;
|
|
||||||
if (error) return <div>Error loading categories</div>;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.dropdownContainer} ref={dropdownRef}>
|
<div className={styles.dropdownContainer} ref={dropdownRef}>
|
||||||
<button onClick={handleToggle} className={styles.navButton}>
|
<button
|
||||||
|
onClick={() => setIsOpen((p) => !p)}
|
||||||
|
className={`${styles.navButton} ${isOpen ? styles.navButtonActive : ""}`}
|
||||||
|
aria-expanded={isOpen}
|
||||||
|
>
|
||||||
<CategoryIcon />
|
<CategoryIcon />
|
||||||
{t("navbar.category")}
|
{isLoading
|
||||||
|
? <div className={styles.loadingDots}><span/><span/><span/></div>
|
||||||
|
: t("navbar.category")
|
||||||
|
}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
|
<>
|
||||||
|
<div className={styles.overlay} onClick={() => setIsOpen(false)} />
|
||||||
|
|
||||||
<div className={styles.dropdownWrapper}>
|
<div className={styles.dropdownWrapper}>
|
||||||
<div className={styles.dropdownPanel} onMouseLeave={handleMouseLeave}>
|
<div className={styles.dropdownPanel}>
|
||||||
<div className={styles.categoriesList}>
|
<div className={styles.categoriesList}>
|
||||||
{categories.map((category) => (
|
{categories.map((cat) => (
|
||||||
<div
|
<div
|
||||||
key={category.id}
|
key={cat.id}
|
||||||
className={`${styles.categoryItem} ${
|
className={`${styles.categoryItem} ${
|
||||||
activeMainCategory?.id === category.id ? styles.active : ""
|
activeCategory?.id === cat.id ? styles.active : ""
|
||||||
}`}
|
}`}
|
||||||
onMouseEnter={() => setActiveMainCategory(category)}
|
onMouseEnter={() => setActiveCategory(cat)}
|
||||||
onClick={() => handleCategorySelect(category)}
|
onClick={() => handleSelect(cat)}
|
||||||
>
|
>
|
||||||
<span className={styles.title}>{category.name}</span>
|
<span className={styles.title}>{cat.name}</span>
|
||||||
|
{cat.children?.length > 0 && (
|
||||||
|
<ChevronRight size={14} className={styles.chevron} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{activeMainCategory && (
|
<ContentPanel
|
||||||
<div className={styles.contentPanel}>
|
category={activeCategory}
|
||||||
<h2
|
onSelect={handleSelect}
|
||||||
onClick={() => handleCategorySelect(activeMainCategory)}
|
onClose={() => setIsOpen(false)}
|
||||||
className={styles.title}
|
|
||||||
>
|
|
||||||
{activeMainCategory.name}
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<div className={styles.subCategoriesContainer}>
|
|
||||||
{activeMainCategory.children &&
|
|
||||||
activeMainCategory.children.length > 0 ? (
|
|
||||||
activeMainCategory.children.map((subcategory) => (
|
|
||||||
<NestedCategory
|
|
||||||
key={subcategory.id}
|
|
||||||
category={subcategory}
|
|
||||||
handleCategorySelect={handleCategorySelect}
|
|
||||||
closeDropdown={() => setIsOpen(false)}
|
|
||||||
/>
|
/>
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<div className={styles.noSubcategories}>
|
|
||||||
{/* No subcategories available */}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import styles from "./Checkout.module.scss";
|
import styles from "./Checkout.module.scss";
|
||||||
import { X } from "lucide-react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {
|
import {
|
||||||
usePlaceOrderMutation,
|
usePlaceOrderMutation,
|
||||||
@@ -9,202 +8,145 @@ import {
|
|||||||
} from "../../app/api/orderApi";
|
} from "../../app/api/orderApi";
|
||||||
import { useGetLocationsQuery } from "../../app/api/locationApi";
|
import { useGetLocationsQuery } from "../../app/api/locationApi";
|
||||||
|
|
||||||
|
const isPriceZero = (price) => !price || parseFloat(price) === 0;
|
||||||
|
|
||||||
const useDeviceType = () => {
|
const useDeviceType = () => {
|
||||||
const [deviceType, setDeviceType] = useState("desktop");
|
const [deviceType, setDeviceType] = useState("desktop");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const userAgent = navigator.userAgent;
|
setDeviceType(
|
||||||
if (/Mobi|Android/i.test(userAgent)) {
|
/Mobi|Android/i.test(navigator.userAgent) ? "mobile" : "desktop",
|
||||||
setDeviceType("mobile");
|
);
|
||||||
} else {
|
|
||||||
setDeviceType("desktop");
|
|
||||||
}
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return deviceType;
|
return deviceType;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Checkout = ({ cartItems, shippingPrice, productIds, onBackToCart, onPlaceOrder }) => {
|
const Checkout = ({
|
||||||
|
cartItems,
|
||||||
|
shippingPrice,
|
||||||
|
productIds,
|
||||||
|
onBackToCart,
|
||||||
|
onPlaceOrder,
|
||||||
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
customer_name: "",
|
customer_name: "",
|
||||||
customer_phone: "",
|
customer_phone: "+993 ",
|
||||||
customer_address: "",
|
customer_address: "",
|
||||||
deliveryAddress: "null",
|
|
||||||
payment_type_id: "",
|
payment_type_id: "",
|
||||||
notes: "",
|
notes: "",
|
||||||
region: "",
|
region: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
const [selectedAddress, setSelectedAddress] = useState(null);
|
const [selectedAddress, setSelectedAddress] = useState(null);
|
||||||
const [placeOrder, { isLoading: isPlacingOrder }] = usePlaceOrderMutation();
|
const [placeOrder] = usePlaceOrderMutation();
|
||||||
const { data: orderTimes = {} } = useGetOrderTimesQuery();
|
|
||||||
const { data: orderPayments = [] } = useGetOrderPaymentsQuery();
|
const { data: orderPayments = [] } = useGetOrderPaymentsQuery();
|
||||||
const { data: locationsData } = useGetLocationsQuery();
|
const { data: locationsData } = useGetLocationsQuery();
|
||||||
const deviceType = useDeviceType();
|
const deviceType = useDeviceType();
|
||||||
|
|
||||||
|
// Sepetteki tüm ürünlerin fiyatı 0 mı?
|
||||||
|
const allItemsZeroPrice = cartItems?.every((item) =>
|
||||||
|
isPriceZero(item.product?.price_amount),
|
||||||
|
);
|
||||||
|
|
||||||
const handleInputChange = (e) => {
|
const handleInputChange = (e) => {
|
||||||
const { name, value } = e.target;
|
const { name, value } = e.target;
|
||||||
|
|
||||||
if (name === "customer_phone") {
|
if (name === "customer_phone") {
|
||||||
// Always keep the +993 prefix
|
|
||||||
const prefix = "+993 ";
|
const prefix = "+993 ";
|
||||||
|
if (value.length < prefix.length) return;
|
||||||
|
|
||||||
// If user is trying to delete the prefix, prevent it
|
const digits = value
|
||||||
if (value.length < prefix.length) {
|
.substring(prefix.length)
|
||||||
return; // Don't update state, keep the current value
|
.replace(/\D/g, "")
|
||||||
}
|
.substring(0, 8);
|
||||||
|
let formatted = prefix + digits.substring(0, 2);
|
||||||
|
if (digits.length > 2) formatted += " " + digits.substring(2);
|
||||||
|
|
||||||
// Extract only the digits after the prefix
|
setFormData((prev) => ({ ...prev, [name]: formatted }));
|
||||||
const inputWithoutPrefix = value.substring(prefix.length).replace(/\D/g, "");
|
|
||||||
|
|
||||||
// Limit to 8 digits max (Turkmenistan mobile number format)
|
|
||||||
const limitedDigits = inputWithoutPrefix.substring(0, 8);
|
|
||||||
|
|
||||||
// Format with space after first 2 digits
|
|
||||||
let formattedPhone = prefix;
|
|
||||||
if (limitedDigits.length > 0) {
|
|
||||||
formattedPhone += limitedDigits.substring(0, 2);
|
|
||||||
|
|
||||||
if (limitedDigits.length > 2) {
|
|
||||||
formattedPhone += " " + limitedDigits.substring(2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setFormData((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[name]: formattedPhone,
|
|
||||||
}));
|
|
||||||
} else {
|
} else {
|
||||||
setFormData((prev) => ({
|
setFormData((prev) => ({ ...prev, [name]: value }));
|
||||||
...prev,
|
|
||||||
[name]: value,
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddressSelect = (value) => {
|
const handleAddressSelect = (value) => {
|
||||||
setSelectedAddress(value);
|
setSelectedAddress(value);
|
||||||
const selectedLocation = locationsData?.data?.find(
|
const selectedLocation = locationsData?.data?.find((l) => l.name === value);
|
||||||
(location) => location.name === value
|
|
||||||
);
|
|
||||||
|
|
||||||
setFormData((prev) => ({
|
setFormData((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
address: value,
|
address: value,
|
||||||
region: selectedLocation ? selectedLocation.region : "",
|
region: selectedLocation?.region || "",
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initialize phone with prefix
|
|
||||||
useEffect(() => {
|
|
||||||
setFormData(prev => ({
|
|
||||||
...prev,
|
|
||||||
customer_phone: "+993 "
|
|
||||||
}));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const formatPhoneNumber = (phoneNumber) => {
|
|
||||||
// Remove the +993 prefix and any spaces
|
|
||||||
return phoneNumber.replace(/^\+993\s*/, "").replace(/\s+/g, "");
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClearAddress = () => {
|
const handleClearAddress = () => {
|
||||||
setSelectedAddress(null);
|
setSelectedAddress(null);
|
||||||
setFormData((prev) => ({
|
setFormData((prev) => ({ ...prev, address: "" }));
|
||||||
...prev,
|
|
||||||
address: "",
|
|
||||||
}));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFocus = (event) => {
|
const handleFocus = (e) =>
|
||||||
event.target.scrollIntoView({
|
e.target.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||||
behavior: "smooth",
|
|
||||||
block: "center",
|
const formatPhoneNumber = (phone) =>
|
||||||
});
|
phone.replace(/^\+993\s*/, "").replace(/\s+/g, "");
|
||||||
};
|
|
||||||
|
|
||||||
const getOrderData = () => {
|
const getOrderData = () => {
|
||||||
// Validation checks
|
|
||||||
if (
|
if (
|
||||||
!formData.customer_name ||
|
!formData.customer_name ||
|
||||||
!formData.customer_phone ||
|
!formData.customer_phone ||
|
||||||
!formData.customer_address ||
|
!formData.customer_address ||
|
||||||
!formData.payment_type_id
|
!formData.payment_type_id
|
||||||
) {
|
) {
|
||||||
console.error("Missing required fields");
|
|
||||||
alert("Please fill in all required fields");
|
alert("Please fill in all required fields");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set default values for delivery
|
const currentDate = new Date().toISOString().split("T")[0];
|
||||||
const currentDate = new Date().toISOString().split('T')[0];
|
|
||||||
const defaultTimeSlot = {
|
|
||||||
date: currentDate,
|
|
||||||
hour: "12:00-14:00" // Default time slot
|
|
||||||
};
|
|
||||||
|
|
||||||
// Prepare data in the format expected by the API
|
|
||||||
return {
|
return {
|
||||||
customer_name: formData.customer_name,
|
customer_name: formData.customer_name,
|
||||||
customer_phone: formatPhoneNumber(formData.customer_phone),
|
customer_phone: formatPhoneNumber(formData.customer_phone),
|
||||||
customer_address: formData.customer_address,
|
customer_address: formData.customer_address,
|
||||||
shipping_method: "standard", // Default to standard shipping
|
shipping_method: "standard",
|
||||||
payment_type_id: formData.payment_type_id,
|
payment_type_id: formData.payment_type_id,
|
||||||
delivery_time: defaultTimeSlot.hour,
|
delivery_time: "12:00-14:00",
|
||||||
delivery_at: defaultTimeSlot.date,
|
delivery_at: currentDate,
|
||||||
region: formData.region || "",
|
region: formData.region || "",
|
||||||
notes: formData.notes || "",
|
notes: formData.notes || "",
|
||||||
// Add shipping price and product IDs
|
|
||||||
shipping_price: shippingPrice,
|
shipping_price: shippingPrice,
|
||||||
product_ids: productIds // Array of product IDs [1, 3, 4, etc.]
|
product_ids: productIds,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create the place order function
|
|
||||||
const handlePlaceOrder = async () => {
|
const handlePlaceOrder = async () => {
|
||||||
const orderDetails = getOrderData();
|
const orderDetails = getOrderData();
|
||||||
if (!orderDetails) return false;
|
if (!orderDetails) return false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await placeOrder(orderDetails).unwrap();
|
await placeOrder(orderDetails).unwrap();
|
||||||
|
|
||||||
console.log("Order placed successfully:", response);
|
|
||||||
window.location.href = "/orders";
|
window.location.href = "/orders";
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to place order:", error);
|
const isHtmlResponse =
|
||||||
|
|
||||||
if (
|
|
||||||
error.data &&
|
error.data &&
|
||||||
typeof error.data === "string" &&
|
typeof error.data === "string" &&
|
||||||
error.data.includes("<!doctype html>")
|
error.data.includes("<!doctype html>");
|
||||||
) {
|
|
||||||
console.error(
|
|
||||||
"Server returned HTML instead of a proper API response"
|
|
||||||
);
|
|
||||||
alert(
|
alert(
|
||||||
"There was a problem with the server. Please try again later or contact support."
|
isHtmlResponse
|
||||||
|
? "There was a problem with the server. Please try again later."
|
||||||
|
: "Failed to place order. Please check your information and try again.",
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
alert(
|
|
||||||
"Failed to place order. Please check your information and try again."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Expose the function to parent component via callback
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (onPlaceOrder) {
|
if (onPlaceOrder) onPlaceOrder(handlePlaceOrder);
|
||||||
onPlaceOrder(handlePlaceOrder);
|
|
||||||
}
|
|
||||||
}, [formData, shippingPrice, productIds]);
|
}, [formData, shippingPrice, productIds]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.checkoutContainer}>
|
<div className={styles.checkoutContainer}>
|
||||||
<h2>{t("cart.basket")} ({cartItems?.length || 0})</h2>
|
{/* <h2>{t("cart.basket")} ({cartItems?.length || 0})</h2> */}
|
||||||
<div className={styles.formSection}>
|
<div className={styles.formSection}>
|
||||||
<div className={styles.paymentOptions}>
|
<div className={styles.paymentOptions}>
|
||||||
<h3>{t("checkout.paymentMethod")}:</h3>
|
<h3>{t("checkout.paymentMethod")}:</h3>
|
||||||
@@ -221,22 +163,22 @@ const Checkout = ({ cartItems, shippingPrice, productIds, onBackToCart, onPlaceO
|
|||||||
<label
|
<label
|
||||||
htmlFor={`payment${payment.id}`}
|
htmlFor={`payment${payment.id}`}
|
||||||
className={styles.customRadio}
|
className={styles.customRadio}
|
||||||
></label>
|
/>
|
||||||
<div
|
<div
|
||||||
className={styles.text}
|
className={styles.text}
|
||||||
onClick={() => {
|
onClick={() =>
|
||||||
setFormData((prev) => ({
|
setFormData((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
payment_type_id: String(payment.id),
|
payment_type_id: String(payment.id),
|
||||||
}));
|
}))
|
||||||
}}
|
}
|
||||||
>
|
>
|
||||||
<span className={styles.optionTitle}>{payment.name}</span>
|
<span className={styles.optionTitle}>{payment.name}</span>
|
||||||
<span className={styles.optionDesc}>
|
{/* <span className={styles.optionDesc}>
|
||||||
{payment.name === "Nagt"
|
{payment.name === "Nagt"
|
||||||
? t("checkout.payment_in_cash_upon_delivery_of_the_order")
|
? t("checkout.payment_in_cash_upon_delivery_of_the_order")
|
||||||
: t("checkout.payment_by_card")}
|
: t("checkout.payment_by_card")}
|
||||||
</span>
|
</span> */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -256,7 +198,6 @@ const Checkout = ({ cartItems, shippingPrice, productIds, onBackToCart, onPlaceO
|
|||||||
onFocus={handleFocus}
|
onFocus={handleFocus}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.formGroup}>
|
<div className={styles.formGroup}>
|
||||||
<label>{t("checkout.telephone")}*</label>
|
<label>{t("checkout.telephone")}*</label>
|
||||||
<input
|
<input
|
||||||
@@ -270,7 +211,6 @@ const Checkout = ({ cartItems, shippingPrice, productIds, onBackToCart, onPlaceO
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.formRow}>
|
<div className={styles.formRow}>
|
||||||
<div className={styles.formGroup}>
|
<div className={styles.formGroup}>
|
||||||
<label>{t("checkout.moreAboutYourAddress")}*</label>
|
<label>{t("checkout.moreAboutYourAddress")}*</label>
|
||||||
@@ -283,7 +223,6 @@ const Checkout = ({ cartItems, shippingPrice, productIds, onBackToCart, onPlaceO
|
|||||||
onFocus={handleFocus}
|
onFocus={handleFocus}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.formGroup}>
|
<div className={styles.formGroup}>
|
||||||
<label>{t("checkout.note")}</label>
|
<label>{t("checkout.note")}</label>
|
||||||
<input
|
<input
|
||||||
@@ -301,22 +240,17 @@ const Checkout = ({ cartItems, shippingPrice, productIds, onBackToCart, onPlaceO
|
|||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
{t(
|
{t(
|
||||||
"checkout.Delivery_is_carried_out_in_the_cities_of_Ashgabat_Buzmein_and_Anau"
|
"checkout.Delivery_is_carried_out_in_the_cities_of_Ashgabat_Buzmein_and_Anau",
|
||||||
)}
|
)}
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
{t(
|
{t(
|
||||||
"checkout.The_minimum_order_amount_must_be_at_least_50_manat_for_orders_over_150_manat_delivery_is_free"
|
"checkout.After_you_place_an_order_on_the_website_the_operator_will_call_you_to_confirm_the_order_for_regular_customers_confirmation_is_carried_out_automatically_at_their_request",
|
||||||
)}
|
)}
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
{t(
|
{t(
|
||||||
"checkout.After_you_place_an_order_on_the_website_the_operator_will_call_you_to_confirm_the_order_for_regular_customers_confirmation_is_carried_out_automatically_at_their_request"
|
"checkout.Payment_is_made_after_you_check_and_accept_the_order_The_amount_of_your_payment_is_indicated_on_the_delivery_persons_payment_document_Payment_is_made_in_cash_and_by_card_in_national_currency_Accepted_and_paid_goods_are_not_subject_to_return",
|
||||||
)}
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
{t(
|
|
||||||
"checkout.Payment_is_made_after_you_check_and_accept_the_order_The_amount_of_your_payment_is_indicated_on_the_delivery_persons_payment_document_Payment_is_made_in_cash_and_by_card_in_national_currency_Accepted_and_paid_goods_are_not_subject_to_return"
|
|
||||||
)}
|
)}
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
206
src/components/FlashSales/FlashSales.module.scss
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
// ── Design tokens ──────────────────────────────────────────────────────────
|
||||||
|
$flash-red: #e53935;
|
||||||
|
$flash-dark-red: #c62828;
|
||||||
|
$flash-accent: #ff6b6b;
|
||||||
|
$flash-yellow: #FFD54F;
|
||||||
|
$white: #fff;
|
||||||
|
|
||||||
|
// ── Section wrapper ────────────────────────────────────────────────────────
|
||||||
|
.flashSales {
|
||||||
|
margin: 28px 0;
|
||||||
|
border-radius: 12px;
|
||||||
|
// overflow: hidden;
|
||||||
|
background: $white;
|
||||||
|
box-shadow: 0 4px 24px rgba(229, 57, 53, 0.15);
|
||||||
|
border: 1.5px solid rgba(229, 57, 53, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Gradient header ────────────────────────────────────────────────────────
|
||||||
|
.header {
|
||||||
|
padding: 14px 20px;
|
||||||
|
background: linear-gradient(120deg, $flash-red 0%, $flash-dark-red 100%);
|
||||||
|
gap: 12px;
|
||||||
|
border-top-left-radius: 8px;
|
||||||
|
border-top-right-radius: 8px;
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
padding: 10px 14px;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Left: label ────────────────────────────────────────────────────────────
|
||||||
|
.flashLabel {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zapWrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: rgba(255, 255, 255, 0.18);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zapIcon {
|
||||||
|
color: $flash-yellow;
|
||||||
|
filter: drop-shadow(0 0 6px rgba(255, 213, 79, 0.8));
|
||||||
|
animation: zapPulse 1s ease-in-out infinite alternate;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes zapPulse {
|
||||||
|
from { opacity: 0.75; transform: scale(1); }
|
||||||
|
to { opacity: 1; transform: scale(1.2); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.flashText {
|
||||||
|
font-size: 1.35rem;
|
||||||
|
font-weight: 900;
|
||||||
|
color: $white;
|
||||||
|
letter-spacing: 2.5px;
|
||||||
|
text-shadow: 0 2px 6px rgba(0, 0, 0, 0.25);
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
font-size: 1.05rem;
|
||||||
|
letter-spacing: 1.5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.saleTitle {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
font-weight: 400;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 220px;
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Right: timer ───────────────────────────────────────────────────────────
|
||||||
|
.timerWrapper {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timerLabel {
|
||||||
|
font-size: 0.78rem;
|
||||||
|
color: rgba(255, 255, 255, 0.85);
|
||||||
|
white-space: nowrap;
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.timerBlock {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: rgba(0, 0, 0, 0.28);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||||
|
border-radius: 7px;
|
||||||
|
padding: 5px 11px;
|
||||||
|
min-width: 44px;
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
min-width: 36px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.timerDigit {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 800;
|
||||||
|
color: $white;
|
||||||
|
line-height: 1;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.timerUnit {
|
||||||
|
font-size: 0.58rem;
|
||||||
|
color: rgba(255, 255, 255, 0.65);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
line-height: 1;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timerSep {
|
||||||
|
color: $white;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1;
|
||||||
|
margin-bottom: 10px; // optical alignment with digit row
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Swiper container ───────────────────────────────────────────────────────
|
||||||
|
.swiperWrapper {
|
||||||
|
padding: 16px 24px;
|
||||||
|
background: #fff8f8;
|
||||||
|
border-bottom-left-radius: 8px;
|
||||||
|
border-bottom-right-radius: 8px;
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
padding: 12px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigation arrows
|
||||||
|
:global(.swiper-button-next),
|
||||||
|
:global(.swiper-button-prev) {
|
||||||
|
color: $flash-red !important;
|
||||||
|
background: $white;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
box-shadow: 0 2px 10px rgba(229, 57, 53, 0.25);
|
||||||
|
transition: background 0.2s, color 0.2s;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: $flash-red !important;
|
||||||
|
color: $white !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.swiper-button-disabled) {
|
||||||
|
opacity: 0.3;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.swiper {
|
||||||
|
padding: 6px 2px 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide {
|
||||||
|
height: auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
// Make ProductCard fill the slide height
|
||||||
|
> * {
|
||||||
|
// height: 100%;
|
||||||
|
// min-height: 100%;
|
||||||
|
// max-height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
148
src/components/FlashSales/index.jsx
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { Swiper, SwiperSlide } from "swiper/react";
|
||||||
|
import { Navigation } from "swiper/modules";
|
||||||
|
import "swiper/css";
|
||||||
|
import "swiper/css/navigation";
|
||||||
|
import { Zap } from "lucide-react";
|
||||||
|
import { Flex } from "antd";
|
||||||
|
import { useGetFlashSalesQuery } from "../../app/api/flashSalesApi";
|
||||||
|
import ProductCard from "../ProductCard";
|
||||||
|
import styles from "./FlashSales.module.scss";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
const parseTime = (timeStr) => {
|
||||||
|
if (!timeStr) return { hours: "00", minutes: "00", seconds: "00" };
|
||||||
|
const parts = timeStr.split(":");
|
||||||
|
return {
|
||||||
|
hours: parts[0] || "00",
|
||||||
|
minutes: parts[1] || "00",
|
||||||
|
seconds: parts[2] || "00",
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const FlashSales = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { data, isLoading, isError } = useGetFlashSalesQuery();
|
||||||
|
const [timers, setTimers] = useState([]);
|
||||||
|
|
||||||
|
const getTimeLeft = (end) => {
|
||||||
|
const endTime = new Date(end).getTime();
|
||||||
|
const now = new Date().getTime();
|
||||||
|
let diff = endTime - now;
|
||||||
|
if (diff <= 0) return "00:00:00";
|
||||||
|
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
|
||||||
|
diff = diff % (1000 * 60 * 60 * 24);
|
||||||
|
const hours = String(Math.floor(diff / (1000 * 60 * 60))).padStart(2, "0");
|
||||||
|
const minutes = String(Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60))).padStart(2, "0");
|
||||||
|
const seconds = String(Math.floor((diff % (1000 * 60)) / 1000)).padStart(2, "0");
|
||||||
|
if (days > 0) {
|
||||||
|
return `${days}g ${hours}:${minutes}:${seconds}`;
|
||||||
|
}
|
||||||
|
return `${hours}:${minutes}:${seconds}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!data?.data) return;
|
||||||
|
const updateTimers = () => {
|
||||||
|
setTimers(data.data.map((flashSale) => getTimeLeft(flashSale.ends_at)));
|
||||||
|
};
|
||||||
|
updateTimers();
|
||||||
|
const interval = setInterval(updateTimers, 1000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
if (isLoading || isError || !data?.data?.length) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{data.data.map((flashSale, idx) => {
|
||||||
|
// Timer parse
|
||||||
|
let days = 0, hours = "00", minutes = "00", seconds = "00";
|
||||||
|
if (timers[idx]) {
|
||||||
|
const match = timers[idx].match(/(?:(\d+)g )?(\d{2}):(\d{2}):(\d{2})/);
|
||||||
|
if (match) {
|
||||||
|
days = match[1] ? Number(match[1]) : 0;
|
||||||
|
hours = match[2];
|
||||||
|
minutes = match[3];
|
||||||
|
seconds = match[4];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className={styles.flashSales} key={flashSale.id}>
|
||||||
|
{/* ── Header ── */}
|
||||||
|
<Flex
|
||||||
|
align="center"
|
||||||
|
justify="space-between"
|
||||||
|
wrap="wrap"
|
||||||
|
className={styles.header}
|
||||||
|
>
|
||||||
|
{/* Left: icon + title */}
|
||||||
|
<Flex align="center" gap={10} className={styles.flashLabel}>
|
||||||
|
<span className={styles.zapWrapper}>
|
||||||
|
<Zap size={22} className={styles.zapIcon} />
|
||||||
|
</span>
|
||||||
|
<span className={styles.flashText}>{t("flashSales.flash_sale")}</span>
|
||||||
|
{flashSale.title && (
|
||||||
|
<span className={styles.saleTitle}>{flashSale.title}</span>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
{/* Right: countdown timer */}
|
||||||
|
<Flex align="center" gap={8} className={styles.timerWrapper}>
|
||||||
|
<span className={styles.timerLabel}>{t("flashSales.ends_in")}</span>
|
||||||
|
<Flex align="center" gap={4}>
|
||||||
|
{days > 0 && (
|
||||||
|
<div className={styles.timerBlock}>
|
||||||
|
<span className={styles.timerDigit}>{days}</span>
|
||||||
|
<span className={styles.timerUnit}>{t("flashSales.day")}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={styles.timerBlock}>
|
||||||
|
<span className={styles.timerDigit}>{hours}</span>
|
||||||
|
<span className={styles.timerUnit}>{t("flashSales.hour")}</span>
|
||||||
|
</div>
|
||||||
|
<span className={styles.timerSep}>:</span>
|
||||||
|
<div className={styles.timerBlock}>
|
||||||
|
<span className={styles.timerDigit}>{minutes}</span>
|
||||||
|
<span className={styles.timerUnit}>{t("flashSales.minute")}</span>
|
||||||
|
</div>
|
||||||
|
<span className={styles.timerSep}>:</span>
|
||||||
|
<div className={styles.timerBlock}>
|
||||||
|
<span className={styles.timerDigit}>{seconds}</span>
|
||||||
|
<span className={styles.timerUnit}>{t("flashSales.second")}</span>
|
||||||
|
</div>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
{/* ── Products Carousel ── */}
|
||||||
|
<div className={styles.swiperWrapper}>
|
||||||
|
<Swiper
|
||||||
|
modules={[Navigation]}
|
||||||
|
navigation
|
||||||
|
slidesPerView={4}
|
||||||
|
spaceBetween={16}
|
||||||
|
breakpoints={{
|
||||||
|
0: { slidesPerView: 1.5, spaceBetween: 10 },
|
||||||
|
480: { slidesPerView: 2.2, spaceBetween: 12 },
|
||||||
|
768: { slidesPerView: 3, spaceBetween: 14 },
|
||||||
|
1024: { slidesPerView: 4, spaceBetween: 16 },
|
||||||
|
}}
|
||||||
|
className={styles.swiper}
|
||||||
|
>
|
||||||
|
{flashSale.products.map((product) => (
|
||||||
|
<SwiperSlide key={product.id} className={styles.slide}>
|
||||||
|
<ProductCard product={product} />
|
||||||
|
</SwiperSlide>
|
||||||
|
))}
|
||||||
|
</Swiper>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FlashSales;
|
||||||
@@ -94,7 +94,6 @@ const Footer = () => {
|
|||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<img src={apk} alt="Download APK" className={styles.appLogo} />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
55
src/components/HomeBrands/HomeBrands.module.scss
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
.container {
|
||||||
|
max-width: 1336px;
|
||||||
|
margin: 20px auto;
|
||||||
|
width: 100%;
|
||||||
|
@media screen and (max-width: 1023px) {
|
||||||
|
margin: 10px auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.brandsSwiper {
|
||||||
|
padding-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brandSlide {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brandCard {
|
||||||
|
width: 122px;
|
||||||
|
height: 50px;
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
padding: 8px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.logoFallback {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
.brandCard {
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
}
|
||||||
67
src/components/HomeBrands/index.jsx
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { useGetBrandsQuery } from '../../app/api/brandsApi';
|
||||||
|
import { Swiper, SwiperSlide } from 'swiper/react';
|
||||||
|
import { Autoplay } from 'swiper/modules';
|
||||||
|
import 'swiper/css';
|
||||||
|
import styles from './HomeBrands.module.scss';
|
||||||
|
import { Logo } from '../Icons';
|
||||||
|
|
||||||
|
const HomeBrands = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
// Fetch brands.
|
||||||
|
const { data: brandsData, isLoading } = useGetBrandsQuery({ limit: 100 });
|
||||||
|
|
||||||
|
if (isLoading || !brandsData || brandsData.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<Swiper
|
||||||
|
modules={[Autoplay]}
|
||||||
|
spaceBetween={12}
|
||||||
|
slidesPerView={'auto'}
|
||||||
|
slidesPerGroup={2}
|
||||||
|
loop={true}
|
||||||
|
autoplay={{
|
||||||
|
delay: 3000,
|
||||||
|
disableOnInteraction: false,
|
||||||
|
}}
|
||||||
|
className={styles.brandsSwiper}
|
||||||
|
>
|
||||||
|
{brandsData.map((brand) => (
|
||||||
|
<SwiperSlide key={brand.id} className={styles.brandSlide}>
|
||||||
|
<div
|
||||||
|
className={styles.brandCard}
|
||||||
|
onClick={() => navigate(`/brands/${brand.id}`)}
|
||||||
|
>
|
||||||
|
{brand.media?.[0]?.thumbnail || brand.media?.[0]?.images_800x800 || brand.logo ? (
|
||||||
|
<img
|
||||||
|
src={
|
||||||
|
brand.media?.[0]?.thumbnail ||
|
||||||
|
brand.media?.[0]?.images_800x800 ||
|
||||||
|
brand.logo
|
||||||
|
}
|
||||||
|
alt={brand.name}
|
||||||
|
onError={(e) => {
|
||||||
|
e.target.style.display = "none";
|
||||||
|
e.target.nextSibling.style.display = "flex";
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className={styles.logoFallback}>
|
||||||
|
<Logo width={40} height={40} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={styles.logoFallback} style={{ display: "none" }}>
|
||||||
|
<Logo width={40} height={40} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SwiperSlide>
|
||||||
|
))}
|
||||||
|
</Swiper>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HomeBrands;
|
||||||
|
|
||||||
@@ -199,6 +199,7 @@ export const OrderIcon = () => (
|
|||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 28.35 28.35"
|
viewBox="0 0 28.35 28.35"
|
||||||
data-v-5c1608dd=""
|
data-v-5c1608dd=""
|
||||||
|
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M24.4,16a2.37,2.37,0,0,0-.94-.59V14.22h0v-2.7h0V9a2.27,2.27,0,0,0-.29-1.11L21.72,5.33a2.27,2.27,0,0,0-2-1.15H11A2.27,2.27,0,0,0,9,5.33L7.52,7.91A2.38,2.38,0,0,0,7.23,9v2.5h0v2h0v.81H4.68a1.46,1.46,0,0,0-1.45,1.46v6.86a1.45,1.45,0,0,0,1.45,1.45H7.2a1.46,1.46,0,0,0,1.28-.78h1.68a2.69,2.69,0,0,1,.57.06l3.55.71a4.09,4.09,0,0,0,.85.09,4.24,4.24,0,0,0,.94-.11l4.64-1,3.65-3.5a2.43,2.43,0,0,0,0-3.49Zm-7.17-1.14-.06,0-3.89-.63a4.34,4.34,0,0,0-2.88.55l-1.75.8V11.52a1,1,0,0,1,1-1H21a1,1,0,0,1,1,1v2h0v1.87a2.45,2.45,0,0,0-.94.5L19,17.68c0-.11,0-.23,0-.36A2.64,2.64,0,0,0,17.23,14.89ZM20.49,6,21.93,8.6A.78.78,0,0,1,22,9v.29a2.51,2.51,0,0,0-1-.23h-5V5.59h3.7A.86.86,0,0,1,20.49,6ZM8.76,8.6,10.2,6A.86.86,0,0,1,11,5.59h3.69V9.08h-5a2.51,2.51,0,0,0-1,.23V9A.78.78,0,0,1,8.76,8.6Zm-1.53,14s0,0,0,0H4.68a0,0,0,0,1,0,0V15.78a0,0,0,0,1,0,0H7.2s0,0,0,0v6.86ZM23.39,18.5,20,21.73l-4.26,1a2.85,2.85,0,0,1-1.2,0L11,22a4.8,4.8,0,0,0-.85-.08H8.65V17.14L11,16.06l.08,0a2.87,2.87,0,0,1,2-.38l3.75.6a1.21,1.21,0,0,1,.76,1.08c0,.44,0,1.18-1.77,1.18H14.54a.7.7,0,0,0-.7.71.7.7,0,0,0,.7.7h4L22,17A1,1,0,0,1,23.4,17a1,1,0,0,1,.3.74A1,1,0,0,1,23.39,18.5Z"
|
d="M24.4,16a2.37,2.37,0,0,0-.94-.59V14.22h0v-2.7h0V9a2.27,2.27,0,0,0-.29-1.11L21.72,5.33a2.27,2.27,0,0,0-2-1.15H11A2.27,2.27,0,0,0,9,5.33L7.52,7.91A2.38,2.38,0,0,0,7.23,9v2.5h0v2h0v.81H4.68a1.46,1.46,0,0,0-1.45,1.46v6.86a1.45,1.45,0,0,0,1.45,1.45H7.2a1.46,1.46,0,0,0,1.28-.78h1.68a2.69,2.69,0,0,1,.57.06l3.55.71a4.09,4.09,0,0,0,.85.09,4.24,4.24,0,0,0,.94-.11l4.64-1,3.65-3.5a2.43,2.43,0,0,0,0-3.49Zm-7.17-1.14-.06,0-3.89-.63a4.34,4.34,0,0,0-2.88.55l-1.75.8V11.52a1,1,0,0,1,1-1H21a1,1,0,0,1,1,1v2h0v1.87a2.45,2.45,0,0,0-.94.5L19,17.68c0-.11,0-.23,0-.36A2.64,2.64,0,0,0,17.23,14.89ZM20.49,6,21.93,8.6A.78.78,0,0,1,22,9v.29a2.51,2.51,0,0,0-1-.23h-5V5.59h3.7A.86.86,0,0,1,20.49,6ZM8.76,8.6,10.2,6A.86.86,0,0,1,11,5.59h3.69V9.08h-5a2.51,2.51,0,0,0-1,.23V9A.78.78,0,0,1,8.76,8.6Zm-1.53,14s0,0,0,0H4.68a0,0,0,0,1,0,0V15.78a0,0,0,0,1,0,0H7.2s0,0,0,0v6.86ZM23.39,18.5,20,21.73l-4.26,1a2.85,2.85,0,0,1-1.2,0L11,22a4.8,4.8,0,0,0-.85-.08H8.65V17.14L11,16.06l.08,0a2.87,2.87,0,0,1,2-.38l3.75.6a1.21,1.21,0,0,1,.76,1.08c0,.44,0,1.18-1.77,1.18H14.54a.7.7,0,0,0-.7.71.7.7,0,0,0,.7.7h4L22,17A1,1,0,0,1,23.4,17a1,1,0,0,1,.3.74A1,1,0,0,1,23.39,18.5Z"
|
||||||
@@ -206,7 +207,22 @@ export const OrderIcon = () => (
|
|||||||
></path>
|
></path>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
export const StoreIcon = () => (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="#4b5563"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
width={16}
|
||||||
|
height={16}
|
||||||
|
>
|
||||||
|
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path>
|
||||||
|
<polyline points="9 22 9 12 15 12 15 22"></polyline>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
export const CategoryIcon = () => (
|
export const CategoryIcon = () => (
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@@ -220,7 +236,7 @@ export const CategoryIcon = () => (
|
|||||||
height={20}
|
height={20}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
fill="#4b5563"
|
fill="currentColor"
|
||||||
d="M30 20c0-.796-.316-1.559-.879-2.121A2.996 2.996 0 0 0 27 17h-7c-.796 0-1.559.316-2.121.879A2.996 2.996 0 0 0 17 20v7c0 .796.316 1.559.879 2.121A2.996 2.996 0 0 0 20 30h7c.796 0 1.559-.316 2.121-.879A2.996 2.996 0 0 0 30 27v-7Zm-15 0c0-.796-.316-1.559-.879-2.121A2.996 2.996 0 0 0 12 17H5c-.796 0-1.559.316-2.121.879A2.996 2.996 0 0 0 2 20v7c0 .796.316 1.559.879 2.121A2.996 2.996 0 0 0 5 30h7c.796 0 1.559-.316 2.121-.879A2.996 2.996 0 0 0 15 27v-7Zm13 0v7a.997.997 0 0 1-1 1h-7a.997.997 0 0 1-1-1v-7a.997.997 0 0 1 1-1h7a.997.997 0 0 1 1 1Zm-15 0v7a.997.997 0 0 1-1 1H5a.997.997 0 0 1-1-1v-7a.997.997 0 0 1 1-1h7a.997.997 0 0 1 1 1Zm2-15c0-.796-.316-1.559-.879-2.121A2.996 2.996 0 0 0 12 2H5c-.796 0-1.559.316-2.121.879A2.996 2.996 0 0 0 2 5v7c0 .796.316 1.559.879 2.121A2.996 2.996 0 0 0 5 15h7c.796 0 1.559-.316 2.121-.879A2.996 2.996 0 0 0 15 12V5Zm15 0c0-.796-.316-1.559-.879-2.121A2.996 2.996 0 0 0 27 2h-7c-.796 0-1.559.316-2.121.879A2.996 2.996 0 0 0 17 5v7c0 .796.316 1.559.879 2.121A2.996 2.996 0 0 0 20 15h7c.796 0 1.559-.316 2.121-.879A2.996 2.996 0 0 0 30 12V5ZM13 5v7a.997.997 0 0 1-1 1H5a.997.997 0 0 1-1-1V5a.997.997 0 0 1 1-1h7a.997.997 0 0 1 1 1Zm15 0v7a.997.997 0 0 1-1 1h-7a.997.997 0 0 1-1-1V5a.997.997 0 0 1 1-1h7a.997.997 0 0 1 1 1Z"
|
d="M30 20c0-.796-.316-1.559-.879-2.121A2.996 2.996 0 0 0 27 17h-7c-.796 0-1.559.316-2.121.879A2.996 2.996 0 0 0 17 20v7c0 .796.316 1.559.879 2.121A2.996 2.996 0 0 0 20 30h7c.796 0 1.559-.316 2.121-.879A2.996 2.996 0 0 0 30 27v-7Zm-15 0c0-.796-.316-1.559-.879-2.121A2.996 2.996 0 0 0 12 17H5c-.796 0-1.559.316-2.121.879A2.996 2.996 0 0 0 2 20v7c0 .796.316 1.559.879 2.121A2.996 2.996 0 0 0 5 30h7c.796 0 1.559-.316 2.121-.879A2.996 2.996 0 0 0 15 27v-7Zm13 0v7a.997.997 0 0 1-1 1h-7a.997.997 0 0 1-1-1v-7a.997.997 0 0 1 1-1h7a.997.997 0 0 1 1 1Zm-15 0v7a.997.997 0 0 1-1 1H5a.997.997 0 0 1-1-1v-7a.997.997 0 0 1 1-1h7a.997.997 0 0 1 1 1Zm2-15c0-.796-.316-1.559-.879-2.121A2.996 2.996 0 0 0 12 2H5c-.796 0-1.559.316-2.121.879A2.996 2.996 0 0 0 2 5v7c0 .796.316 1.559.879 2.121A2.996 2.996 0 0 0 5 15h7c.796 0 1.559-.316 2.121-.879A2.996 2.996 0 0 0 15 12V5Zm15 0c0-.796-.316-1.559-.879-2.121A2.996 2.996 0 0 0 27 2h-7c-.796 0-1.559.316-2.121.879A2.996 2.996 0 0 0 17 5v7c0 .796.316 1.559.879 2.121A2.996 2.996 0 0 0 20 15h7c.796 0 1.559-.316 2.121-.879A2.996 2.996 0 0 0 30 12V5ZM13 5v7a.997.997 0 0 1-1 1H5a.997.997 0 0 1-1-1V5a.997.997 0 0 1 1-1h7a.997.997 0 0 1 1 1Zm15 0v7a.997.997 0 0 1-1 1h-7a.997.997 0 0 1-1-1V5a.997.997 0 0 1 1-1h7a.997.997 0 0 1 1 1Z"
|
||||||
></path>
|
></path>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ const Layout = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Navbar />
|
<Navbar />
|
||||||
<NavbarDown/>
|
{/* <NavbarDown/> */}
|
||||||
<main>
|
<main>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
height: 2.5rem;
|
height: 2.5rem;
|
||||||
font-size: 0.875rem;
|
font-size: 16px;
|
||||||
color: #4b5563;
|
color: #4b5563;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
|
|||||||
@@ -14,18 +14,35 @@
|
|||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
margin-bottom: 1px;
|
margin-bottom: 1px;
|
||||||
border-bottom: 3px solid #f3f4f6;
|
border-bottom: 3px solid #f3f4f6;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: max-content;
|
width: max-content;
|
||||||
font-size: 14px;
|
font-size: 16px;
|
||||||
border-radius: 4px;
|
|
||||||
border: #000000;
|
|
||||||
background-color: #000000;
|
|
||||||
padding: 6px 10px;
|
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
|
background-color: #000000;
|
||||||
|
border: 1px solid #000000; // Border rengini belirtirken kalınlık da eklemelisin
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
// Mobil Görünüm (Ortak)
|
||||||
|
@media screen and (max-width: 500px) {
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 8px 10px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__satyjy {
|
||||||
|
@media screen and (max-width: 785px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbarDown {
|
.navbarDown {
|
||||||
@@ -33,7 +50,7 @@
|
|||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
max-width: 1366px;
|
max-width: 1366px;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 80px; // navbarUp yüksekliği kadar
|
||||||
padding-top: 12px;
|
padding-top: 12px;
|
||||||
padding-bottom: 12px;
|
padding-bottom: 12px;
|
||||||
padding-left: 1.375rem;
|
padding-left: 1.375rem;
|
||||||
@@ -48,11 +65,11 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 10px 22px 0px;
|
padding: 10px 22px 0px;
|
||||||
height: 60px;
|
height: 80px;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@media screen and (max-width: 426px) {
|
@media screen and (max-width: 500px) {
|
||||||
height: 40px;
|
height: 40px;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
padding: 10px 15px 6px;
|
padding: 10px 15px 6px;
|
||||||
@@ -66,14 +83,19 @@
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@media screen and (max-width: 426px) {
|
img {
|
||||||
width: 80px;
|
width: 300px;
|
||||||
|
@media screen and (max-width: 500px) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media screen and (max-width: 500px) {
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.stick {
|
.stick {
|
||||||
@@ -85,6 +107,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.navLinks {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
.navLinks ul {
|
.navLinks ul {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -106,8 +131,8 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
svg {
|
svg {
|
||||||
fill: #4b5563;
|
fill: #4b5563;
|
||||||
width: 20px;
|
width: 24px;
|
||||||
height: 20px;
|
height: 24px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.searchWrapper {
|
.searchWrapper {
|
||||||
@@ -116,7 +141,6 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
flex-direction: row-reverse;
|
flex-direction: row-reverse;
|
||||||
|
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 24px;
|
width: 24px;
|
||||||
@@ -154,7 +178,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
height: 2.5rem;
|
height: 2.5rem;
|
||||||
font-size: 0.875rem;
|
font-size: 16px;
|
||||||
color: #4b5563;
|
color: #4b5563;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
@@ -162,6 +186,10 @@
|
|||||||
&:hover {
|
&:hover {
|
||||||
background-color: #f3f4f6;
|
background-color: #f3f4f6;
|
||||||
}
|
}
|
||||||
|
svg {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.cartSection {
|
.cartSection {
|
||||||
@@ -192,7 +220,7 @@
|
|||||||
@media screen and (min-width: 1024px) {
|
@media screen and (min-width: 1024px) {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@media screen and (max-width: 426px) {
|
@media screen and (max-width: 500px) {
|
||||||
padding: 9px 0;
|
padding: 9px 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -250,9 +278,23 @@
|
|||||||
border: none;
|
border: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
|
|
||||||
|
|
||||||
&::placeholder {
|
&::placeholder {
|
||||||
color: #9ca3af;
|
color: #9ca3af;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.langSelector {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-left: auto;
|
||||||
|
@media screen and (max-width: 708px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonsContainer {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin: 8px 14px 6px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { CartIcon, WishlistIcon, BrandIcon, OrderIcon } from "../Icons";
|
import { CartIcon, WishlistIcon, BrandIcon, OrderIcon, StoreIcon } from "../Icons";
|
||||||
import styles from "./Navbar.module.scss";
|
import styles from "./Navbar.module.scss";
|
||||||
import { UserOutlined, LogoutOutlined, HomeOutlined } from "@ant-design/icons";
|
import { UserOutlined, LogoutOutlined, HomeOutlined, ShopOutlined } from "@ant-design/icons";
|
||||||
import { FaGlobe } from "react-icons/fa6";
|
import { FaGlobe } from "react-icons/fa6";
|
||||||
import { Input, Badge, Menu, Dropdown } from "antd";
|
import { Input, Badge, Menu, Dropdown } from "antd";
|
||||||
const { Search } = Input;
|
const { Search } = Input;
|
||||||
@@ -135,7 +135,7 @@ const NavbarDown = () => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className={styles.navbar}>
|
<header className={styles.navbar} style={{ width: "100%" }}>
|
||||||
<div className={styles.navbarDown} style={{ position: "sticky" }}>
|
<div className={styles.navbarDown} style={{ position: "sticky" }}>
|
||||||
<nav className={styles.navLinks}>
|
<nav className={styles.navLinks}>
|
||||||
<ul>
|
<ul>
|
||||||
@@ -151,6 +151,15 @@ const NavbarDown = () => {
|
|||||||
</button>
|
</button>
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
|
<div className={styles.stick}></div>
|
||||||
|
<li>
|
||||||
|
<Link to={"/stores"}>
|
||||||
|
<button className={styles.navButton}>
|
||||||
|
<ShopOutlined />
|
||||||
|
{t("navbar.stores")}
|
||||||
|
</button>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
<li className={styles.searchWrapper}>
|
<li className={styles.searchWrapper}>
|
||||||
<CiSearch />
|
<CiSearch />
|
||||||
<input
|
<input
|
||||||
@@ -255,7 +264,10 @@ const NavbarDown = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className={styles.stick}></div>
|
<div className={styles.stick}></div>
|
||||||
<div className={styles.location}>
|
<div className={styles.location}>
|
||||||
<CiLocationOn /> Aşgabat
|
<Link to={'/stores'} style={{textDecoration: 'none'}}><button className={styles.navButton}>
|
||||||
|
<ShopOutlined />
|
||||||
|
{t("navbar.stores")}
|
||||||
|
</button></Link>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.stick}></div>
|
<div className={styles.stick}></div>
|
||||||
<div className={styles.searchIcon} onClick={toggleSearch}>
|
<div className={styles.searchIcon} onClick={toggleSearch}>
|
||||||
|
|||||||
@@ -4,40 +4,101 @@ import SignupForm from "../BeSeller/index";
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { LogoWithText } from "../Icons";
|
import { LogoWithText } from "../Icons";
|
||||||
import Logo from "../../assets/logo2.png"
|
import Logo from "../../assets/logo2.png";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import tm from "../../assets/tm.png";
|
||||||
|
import ru from "../../assets/ru.png";
|
||||||
|
import en from "../../assets/en.png";
|
||||||
|
import NavbarDown from "./NavbarDown";
|
||||||
const Navbar = () => {
|
const Navbar = () => {
|
||||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const showModal = () => {
|
const { i18n } = useTranslation();
|
||||||
setIsModalVisible(true);
|
|
||||||
|
const changeLanguage = (langCode) => {
|
||||||
|
i18n.changeLanguage(langCode);
|
||||||
|
localStorage.setItem("preferredLanguage", langCode);
|
||||||
|
window.location.reload();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCancel = () => {
|
const languages = [
|
||||||
setIsModalVisible(false);
|
{ code: "tk", flag: tm, label: "TM" },
|
||||||
};
|
{ code: "ru", flag: ru, label: "RU" },
|
||||||
|
{ code: "en", flag: en, label: "EN" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const showModal = () => setIsModalVisible(true);
|
||||||
|
const handleCancel = () => setIsModalVisible(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<header className={styles.navbar}>
|
<header className={styles.navbar}>
|
||||||
<div className={styles.navbarUp}>
|
<div className={styles.navbarUp}>
|
||||||
<div
|
<div
|
||||||
style={{ maxWidth: "1366px", display: "flex", margin: "0 auto", alignItems: "center"}}
|
style={{
|
||||||
|
maxWidth: "1366px",
|
||||||
|
display: "flex",
|
||||||
|
margin: "0 auto",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div className={styles.logo}>
|
<div className={styles.logo}>
|
||||||
<div
|
<div
|
||||||
className={styles.logoContainer}
|
className={styles.logoContainer}
|
||||||
onClick={() => navigate("/")}
|
onClick={() => navigate("/")}
|
||||||
>
|
>
|
||||||
{/* <LogoWithText /> */}
|
<img src={Logo} alt="" />
|
||||||
<img style={{width: "200px"}} src={Logo} alt="" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: "flex", alignItems: "center", padding: "8px 14px 6px" }}>
|
<div className={styles.langSelector}>
|
||||||
|
{languages.map((lang) => (
|
||||||
|
<button
|
||||||
|
key={lang.code}
|
||||||
|
onClick={() => changeLanguage(lang.code)}
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
fontSize: "16px",
|
||||||
|
gap: "4px",
|
||||||
|
background:
|
||||||
|
i18n.language === lang.code ? "#f0f0f0" : "transparent",
|
||||||
|
border:
|
||||||
|
i18n.language === lang.code
|
||||||
|
? "1px solid #d9d9d9"
|
||||||
|
: "1px solid transparent",
|
||||||
|
borderRadius: "4px",
|
||||||
|
padding: "6px 10px",
|
||||||
|
cursor: "pointer",
|
||||||
|
fontWeight: i18n.language === lang.code ? "600" : "400",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={lang.flag}
|
||||||
|
alt={lang.label}
|
||||||
|
style={{ width: "20px" }}
|
||||||
|
/>
|
||||||
|
{lang.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className={styles.buttonsContainer}>
|
||||||
<button className={styles.btn} onClick={showModal}>
|
<button className={styles.btn} onClick={showModal}>
|
||||||
Satyjy bol
|
Satyjy bol
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
className={`${styles.btn} ${styles.btn__satyjy}`}
|
||||||
|
onClick={() => {
|
||||||
|
window.location.href = "/panel";
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Satyjy
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<NavbarDown />
|
||||||
</header>
|
</header>
|
||||||
<Modal
|
<Modal
|
||||||
open={isModalVisible}
|
open={isModalVisible}
|
||||||
|
|||||||
102
src/components/PendingPriceBadge/PendingPriceBadge.module.scss
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
.pendingPriceBadgeWrapper {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pendingPriceBadge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #faeeda;
|
||||||
|
border: 0.5px solid #ef9f27;
|
||||||
|
color: #854f0b;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pendingPriceTooltip {
|
||||||
|
position: absolute;
|
||||||
|
bottom: calc(100% + 6px);
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background: var(--color-background-primary, #ffffff);
|
||||||
|
border: 0.5px solid var(--color-border-secondary, #e2e2e2);
|
||||||
|
border-radius: var(--border-radius-md, 6px);
|
||||||
|
padding: 8px 12px;
|
||||||
|
width: 220px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--color-text-primary, #333333);
|
||||||
|
line-height: 1.5;
|
||||||
|
z-index: 100;
|
||||||
|
white-space: normal;
|
||||||
|
pointer-events: none;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
strong {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
color: var(--color-text-primary, #000000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:global {
|
||||||
|
.pending-price-modal {
|
||||||
|
.ant-modal-content {
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-modal-header {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
.ant-modal-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-modal-body {
|
||||||
|
p {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #555;
|
||||||
|
margin: 0;
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-modal-footer {
|
||||||
|
margin-top: 20px;
|
||||||
|
.ant-btn-primary {
|
||||||
|
background-color: #888888;
|
||||||
|
border-color: #888888;
|
||||||
|
border-radius: 6px;
|
||||||
|
height: 36px;
|
||||||
|
padding: 0 20px;
|
||||||
|
font-weight: 500;
|
||||||
|
&:hover {
|
||||||
|
background-color: #666666;
|
||||||
|
border-color: #666666;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
71
src/components/PendingPriceBadge/index.jsx
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { Modal } from "antd";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import styles from "./PendingPriceBadge.module.scss";
|
||||||
|
|
||||||
|
const PendingPriceModal = ({ open, onClose, t }) => (
|
||||||
|
<Modal
|
||||||
|
open={open}
|
||||||
|
onOk={onClose}
|
||||||
|
onCancel={onClose}
|
||||||
|
okText={t("common.ok") || "Ok"}
|
||||||
|
cancelButtonProps={{ style: { display: "none" } }}
|
||||||
|
centered
|
||||||
|
title={t("cart.pendingPriceTitle") || "Bahasy anyklamaly"}
|
||||||
|
className="pending-price-modal"
|
||||||
|
width={400}
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
{t("cart.pendingPriceDesc") ||
|
||||||
|
"Bu sargytdaky bir ýa-da birnäçe harydyň bahasy entek kesgitlenmedik. Operatorymyz siziň bilen habarlaşyp, goşmaça maglumat berer."}
|
||||||
|
</p>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
|
||||||
|
const PendingPriceBadge = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [tooltipVisible, setTooltipVisible] = useState(false);
|
||||||
|
const [modalVisible, setModalVisible] = useState(false);
|
||||||
|
|
||||||
|
const handleClick = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopPropagation = (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span onClick={stopPropagation}>
|
||||||
|
<span
|
||||||
|
className={styles.pendingPriceBadgeWrapper}
|
||||||
|
onMouseEnter={() => setTooltipVisible(true)}
|
||||||
|
onMouseLeave={() => setTooltipVisible(false)}
|
||||||
|
onClick={handleClick}
|
||||||
|
onTouchEnd={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className={styles.pendingPriceBadge}>!</span>
|
||||||
|
|
||||||
|
{tooltipVisible && (
|
||||||
|
<span className={styles.pendingPriceTooltip}>
|
||||||
|
<strong>{t("cart.pendingPriceTitle") || "Bahasyny anyklamaly"}</strong>
|
||||||
|
{t("cart.pendingPriceTooltipDesc") ||
|
||||||
|
"Bu sargytdaky harydyň bahasy kesgitlenmedik. Operator size jaň edip goşmaça maglumat berer."}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<PendingPriceModal
|
||||||
|
open={modalVisible}
|
||||||
|
onClose={() => setModalVisible(false)}
|
||||||
|
t={t}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PendingPriceBadge;
|
||||||
@@ -5,14 +5,14 @@
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
transition: transform 0.2s ease;
|
transition: all 0.3s ease;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
transform: translateY(-4px);
|
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.15);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
@media screen and (max-width: 426px) {
|
@media screen and (max-width: 426px) {
|
||||||
@@ -30,6 +30,7 @@
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
z-index: 1;
|
||||||
@media screen and (max-width: 426px) {
|
@media screen and (max-width: 426px) {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
@@ -71,8 +72,16 @@
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #333;
|
color: #333;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 2.4em;
|
||||||
|
line-height: 1.2;
|
||||||
|
|
||||||
@media screen and (max-width: 426px) {
|
@media screen and (max-width: 426px) {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
height: 2.8em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,9 +90,15 @@
|
|||||||
color: #666;
|
color: #666;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 2.8em;
|
||||||
|
|
||||||
@media screen and (max-width: 1023px) {
|
@media screen and (max-width: 1023px) {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
height: 2.8em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,8 +106,8 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
margin-top: 0.5rem;
|
margin-top: auto;
|
||||||
margin: 0;
|
margin-bottom: 0;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
touch-action: pan-y;
|
touch-action: pan-y;
|
||||||
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.productImage {
|
.productImage {
|
||||||
width: 99%;
|
width: 99%;
|
||||||
height: auto;
|
height: auto;
|
||||||
@@ -31,6 +31,11 @@
|
|||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hovered {
|
||||||
|
transform: scale(1.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Style for images inside detail view */
|
/* Style for images inside detail view */
|
||||||
@@ -145,6 +150,7 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
padding: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.thumbnail {
|
.thumbnail {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const ImageCarousel = ({
|
|||||||
altText,
|
altText,
|
||||||
showThumbnails = false,
|
showThumbnails = false,
|
||||||
isDetailView = false,
|
isDetailView = false,
|
||||||
|
isHovered = false,
|
||||||
}) => {
|
}) => {
|
||||||
const [currentIndex, setCurrentIndex] = useState(0);
|
const [currentIndex, setCurrentIndex] = useState(0);
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
@@ -28,15 +29,15 @@ const ImageCarousel = ({
|
|||||||
: images[0]?.images_1200x1200 || "";
|
: images[0]?.images_1200x1200 || "";
|
||||||
|
|
||||||
// Auto-slide functionality
|
// Auto-slide functionality
|
||||||
useEffect(() => {
|
// useEffect(() => {
|
||||||
if (!hasMultipleImages || isModalOpen) return;
|
// if (!hasMultipleImages || isModalOpen) return;
|
||||||
|
|
||||||
const interval = setInterval(() => {
|
// const interval = setInterval(() => {
|
||||||
setCurrentIndex((prev) => (prev === images.length - 1 ? 0 : prev + 1));
|
// setCurrentIndex((prev) => (prev === images.length - 1 ? 0 : prev + 1));
|
||||||
}, 9000);
|
// }, 9000);
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
// return () => clearInterval(interval);
|
||||||
}, [hasMultipleImages, images, isModalOpen]);
|
// }, [hasMultipleImages, images, isModalOpen]);
|
||||||
|
|
||||||
// Reset zoom/rotation when modal closes or image changes
|
// Reset zoom/rotation when modal closes or image changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -220,7 +221,8 @@ const ImageCarousel = ({
|
|||||||
isDetailView ? styles.detailImage : styles.cardImage
|
isDetailView ? styles.detailImage : styles.cardImage
|
||||||
}`}
|
}`}
|
||||||
onClick={isDetailView ? openModal : undefined}
|
onClick={isDetailView ? openModal : undefined}
|
||||||
style={{ cursor: isDetailView ? "pointer" : "default" }}
|
style={{ cursor: isDetailView ? "pointer" : "default" , transform: isHovered ? "scale(1.05)" : "none" }}
|
||||||
|
|
||||||
/>
|
/>
|
||||||
{isDetailView && renderModal()}
|
{isDetailView && renderModal()}
|
||||||
</div>
|
</div>
|
||||||
@@ -450,7 +452,7 @@ const ImageCarousel = ({
|
|||||||
alt={altText || "Product image"}
|
alt={altText || "Product image"}
|
||||||
className={`${styles.productImage} ${
|
className={`${styles.productImage} ${
|
||||||
isDetailView ? styles.detailImage : styles.cardImage
|
isDetailView ? styles.detailImage : styles.cardImage
|
||||||
}`}
|
} ${isHovered ? styles.hovered : ''}`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -3,36 +3,32 @@ import styles from "./ProductCard.module.scss";
|
|||||||
import { IoMdHeartEmpty, IoMdHeart } from "react-icons/io";
|
import { IoMdHeartEmpty, IoMdHeart } from "react-icons/io";
|
||||||
import { FaShoppingCart } from "react-icons/fa";
|
import { FaShoppingCart } from "react-icons/fa";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { debounce } from "lodash";
|
|
||||||
import {
|
import {
|
||||||
useAddFavoriteMutation,
|
useAddFavoriteMutation,
|
||||||
useRemoveFavoriteMutation,
|
useRemoveFavoriteMutation,
|
||||||
|
useGetFavoritesQuery,
|
||||||
} from "../../app/api/favoritesApi";
|
} from "../../app/api/favoritesApi";
|
||||||
import { useGetFavoritesQuery } from "../../app/api/favoritesApi";
|
|
||||||
import {
|
import {
|
||||||
useAddToCartMutation,
|
useAddToCartMutation,
|
||||||
useUpdateCartItemMutation,
|
useUpdateCartItemMutation,
|
||||||
useRemoveFromCartMutation,
|
useRemoveFromCartMutation,
|
||||||
useGetCartQuery,
|
|
||||||
} from "../../app/api/cartApi";
|
} from "../../app/api/cartApi";
|
||||||
import { Modal } from "antd";
|
import { Modal } from "antd";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { DecreaseIcon, IncreaseIcon } from "../Icons";
|
import { DecreaseIcon, IncreaseIcon } from "../Icons";
|
||||||
import ImageCarousel from "./imageCarousel/index";
|
import ImageCarousel from "./imageCarousel/index";
|
||||||
|
import { useCart } from "../../app/api/useCart";
|
||||||
|
|
||||||
// Helper function to strip HTML tags and truncate text
|
|
||||||
const truncateDescription = (htmlString, maxLength = 80) => {
|
const truncateDescription = (htmlString, maxLength = 80) => {
|
||||||
const tempDiv = document.createElement("div");
|
const tempDiv = document.createElement("div");
|
||||||
tempDiv.innerHTML = htmlString;
|
tempDiv.innerHTML = htmlString;
|
||||||
const textContent = tempDiv.textContent || tempDiv.innerText || "";
|
const textContent = tempDiv.textContent || tempDiv.innerText || "";
|
||||||
const truncatedText =
|
return textContent.length > maxLength
|
||||||
textContent.length > maxLength
|
|
||||||
? textContent.substring(0, maxLength).trim() + "..."
|
? textContent.substring(0, maxLength).trim() + "..."
|
||||||
: textContent;
|
: textContent;
|
||||||
return truncatedText;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
import { useCart } from "../../app/api/useCart";
|
const isPriceZero = (price) => !price || parseFloat(price) === 0;
|
||||||
|
|
||||||
const ProductCard = ({
|
const ProductCard = ({
|
||||||
product,
|
product,
|
||||||
@@ -41,26 +37,24 @@ const ProductCard = ({
|
|||||||
onAddToCart,
|
onAddToCart,
|
||||||
onToggleFavorite,
|
onToggleFavorite,
|
||||||
isFavorite = false,
|
isFavorite = false,
|
||||||
descriptionMaxLength = 85,
|
descriptionMaxLength = 120,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [stockErrorModalVisible, setStockErrorModalVisible] = useState(false);
|
const [stockErrorModalVisible, setStockErrorModalVisible] = useState(false);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
|
|
||||||
const [addFavorite] = useAddFavoriteMutation();
|
const [addFavorite] = useAddFavoriteMutation();
|
||||||
const [removeFavorite] = useRemoveFavoriteMutation();
|
const [removeFavorite] = useRemoveFavoriteMutation();
|
||||||
const { data: favoriteProducts = [] } = useGetFavoritesQuery();
|
const { data: favoriteProducts = [] } = useGetFavoritesQuery();
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
const [localIsFavorite, setLocalIsFavorite] = useState(
|
|
||||||
favoriteProducts.some((fav) => fav.product?.id === product.id),
|
|
||||||
);
|
|
||||||
|
|
||||||
const truncatedDesc = truncateDescription(
|
const [localIsFavorite, setLocalIsFavorite] = useState(
|
||||||
product.description,
|
favoriteProducts.some((fav) => fav.product?.id === product.id)
|
||||||
descriptionMaxLength,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const { getCartItem } = useCart();
|
const { getCartItem } = useCart();
|
||||||
|
|
||||||
const [addToCart] = useAddToCartMutation();
|
const [addToCart] = useAddToCartMutation();
|
||||||
const [updateCartItem] = useUpdateCartItemMutation();
|
const [updateCartItem] = useUpdateCartItemMutation();
|
||||||
const [removeFromCart] = useRemoveFromCartMutation();
|
const [removeFromCart] = useRemoveFromCartMutation();
|
||||||
@@ -69,26 +63,54 @@ const ProductCard = ({
|
|||||||
const [localQuantity, setLocalQuantity] = useState(0);
|
const [localQuantity, setLocalQuantity] = useState(0);
|
||||||
const [pendingQuantity, setPendingQuantity] = useState(0);
|
const [pendingQuantity, setPendingQuantity] = useState(0);
|
||||||
|
|
||||||
// ✅ Cart item değiştiğinde local state'i güncelle
|
const { name, price_amount, old_price_amount, media = [], reviews } = product;
|
||||||
|
|
||||||
|
const truncatedDesc = truncateDescription(product.description, descriptionMaxLength);
|
||||||
|
|
||||||
|
const calculatedDiscount =
|
||||||
|
!product.discount &&
|
||||||
|
old_price_amount &&
|
||||||
|
price_amount &&
|
||||||
|
old_price_amount > price_amount
|
||||||
|
? Math.round(((old_price_amount - price_amount) / old_price_amount) * 100)
|
||||||
|
: null;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const qty = parseInt(
|
const qty = parseInt(cartItem?.quantity || cartItem?.product_quantity || 0, 10);
|
||||||
cartItem?.quantity || cartItem?.product_quantity || 0,
|
|
||||||
10,
|
|
||||||
);
|
|
||||||
setLocalQuantity(qty);
|
setLocalQuantity(qty);
|
||||||
setPendingQuantity(qty);
|
setPendingQuantity(qty);
|
||||||
}, [cartItem]);
|
}, [cartItem]);
|
||||||
|
|
||||||
// ✅ Favorite state'i güncelle
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (Array.isArray(favoriteProducts)) {
|
if (Array.isArray(favoriteProducts)) {
|
||||||
const isFav = favoriteProducts.some(
|
setLocalIsFavorite(
|
||||||
(fav) => fav.product?.id === product.id,
|
favoriteProducts.some((fav) => fav.product?.id === product.id)
|
||||||
);
|
);
|
||||||
setLocalIsFavorite(isFav);
|
|
||||||
}
|
}
|
||||||
}, [favoriteProducts, product.id]);
|
}, [favoriteProducts, product.id]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const serverQty = parseInt(cartItem?.quantity || cartItem?.product_quantity || 0, 10);
|
||||||
|
|
||||||
|
if (pendingQuantity === serverQty || pendingQuantity <= 0) return;
|
||||||
|
|
||||||
|
const handler = setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
await updateCartItem({ productId: product.id, quantity: pendingQuantity }).unwrap();
|
||||||
|
} catch {
|
||||||
|
setLocalQuantity(serverQty);
|
||||||
|
setPendingQuantity(serverQty);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
return () => clearTimeout(handler);
|
||||||
|
}, [pendingQuantity, cartItem, product.id, updateCartItem]);
|
||||||
|
|
||||||
|
const handleCardClick = () => navigate(`/product/${product.id}`);
|
||||||
|
|
||||||
const handleAddToCart = async (event) => {
|
const handleAddToCart = async (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
@@ -98,51 +120,17 @@ const ProductCard = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ Optimistic update
|
|
||||||
setLocalQuantity((prev) => prev + 1);
|
setLocalQuantity((prev) => prev + 1);
|
||||||
setPendingQuantity((prev) => prev + 1);
|
setPendingQuantity((prev) => prev + 1);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await addToCart({ productId: product.id, quantity: 1 }).unwrap();
|
await addToCart({ productId: product.id, quantity: 1 }).unwrap();
|
||||||
// ✅ Başarılı - RTK Query otomatik cache'i güncelleyecek
|
} catch {
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to add to cart:", error);
|
|
||||||
// ✅ Hata varsa geri al
|
|
||||||
setLocalQuantity((prev) => prev - 1);
|
setLocalQuantity((prev) => prev - 1);
|
||||||
setPendingQuantity((prev) => prev - 1);
|
setPendingQuantity((prev) => prev - 1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// ✅ Debounced update - sadece mutation, refetch yok
|
|
||||||
useEffect(() => {
|
|
||||||
const serverQty = parseInt(
|
|
||||||
cartItem?.quantity || cartItem?.product_quantity || 0,
|
|
||||||
10,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (pendingQuantity === serverQty || pendingQuantity <= 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const handler = setTimeout(async () => {
|
|
||||||
try {
|
|
||||||
setIsLoading(true);
|
|
||||||
await updateCartItem({
|
|
||||||
productId: product.id,
|
|
||||||
quantity: pendingQuantity,
|
|
||||||
}).unwrap();
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to update cart item:", error);
|
|
||||||
setLocalQuantity(serverQty);
|
|
||||||
setPendingQuantity(serverQty);
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
}, 500);
|
|
||||||
|
|
||||||
return () => clearTimeout(handler);
|
|
||||||
}, [pendingQuantity, cartItem, product.id, updateCartItem]);
|
|
||||||
|
|
||||||
const handleQuantityIncrease = (event) => {
|
const handleQuantityIncrease = (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
@@ -165,24 +153,17 @@ const ProductCard = ({
|
|||||||
if (isLoading) return;
|
if (isLoading) return;
|
||||||
|
|
||||||
if (pendingQuantity <= 1) {
|
if (pendingQuantity <= 1) {
|
||||||
// ✅ Sıfıra düşünce direkt sil
|
|
||||||
setPendingQuantity(0);
|
setPendingQuantity(0);
|
||||||
setLocalQuantity(0);
|
setLocalQuantity(0);
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
removeFromCart({ productId: product.id })
|
removeFromCart({ productId: product.id })
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.then(() => {
|
|
||||||
// ✅ Başarılı - RTK Query cache'i güncelleyecek
|
|
||||||
})
|
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
// ✅ Hata varsa geri al
|
|
||||||
setLocalQuantity(1);
|
setLocalQuantity(1);
|
||||||
setPendingQuantity(1);
|
setPendingQuantity(1);
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => setIsLoading(false));
|
||||||
setIsLoading(false);
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
setLocalQuantity((prev) => prev - 1);
|
setLocalQuantity((prev) => prev - 1);
|
||||||
setPendingQuantity((prev) => prev - 1);
|
setPendingQuantity((prev) => prev - 1);
|
||||||
@@ -196,61 +177,63 @@ const ProductCard = ({
|
|||||||
if (isLoading) return;
|
if (isLoading) return;
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
setLocalIsFavorite((prev) => !prev);
|
||||||
// ✅ Optimistic update
|
|
||||||
setLocalIsFavorite(!localIsFavorite);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (localIsFavorite) {
|
if (localIsFavorite) {
|
||||||
const result = await removeFavorite(product.id).unwrap();
|
await removeFavorite(product.id).unwrap();
|
||||||
// ✅ Başarılı - RTK Query otomatik güncelleyecek
|
|
||||||
} else {
|
} else {
|
||||||
const result = await addFavorite(product.id).unwrap();
|
await addFavorite(product.id).unwrap();
|
||||||
// ✅ Başarılı - RTK Query otomatik güncelleyecek
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch {
|
||||||
console.error("Failed to toggle favorite:", error);
|
setLocalIsFavorite((prev) => !prev); // revert
|
||||||
// ✅ Hata varsa geri al
|
|
||||||
setLocalIsFavorite(localIsFavorite);
|
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCardClick = () => {
|
|
||||||
navigate(`/product/${product.id}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const { name, price_amount, old_price_amount, media = [], reviews } = product;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={styles.productCard} onClick={handleCardClick}>
|
<div
|
||||||
|
className={styles.productCard}
|
||||||
|
onClick={handleCardClick}
|
||||||
|
onMouseEnter={() => setIsHovered(true)}
|
||||||
|
onMouseLeave={() => setIsHovered(false)}
|
||||||
|
>
|
||||||
<div className={styles.imageContainer}>
|
<div className={styles.imageContainer}>
|
||||||
{product.discount && (
|
{(product.discount > 0 || calculatedDiscount > 0) && (
|
||||||
<span className={styles.discountBadge}>-{product.discount}%</span>
|
<span className={styles.discountBadge}>
|
||||||
|
-{product.discount || calculatedDiscount}%
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
{product.stock === 0 && (
|
{product.stock === 0 && (
|
||||||
<span className={`${styles.discountBadge} ${styles.outOfStock}`}>
|
<span className={`${styles.discountBadge} ${styles.outOfStock}`}>
|
||||||
{t("common.out_of_stock")}
|
{t("common.out_of_stock")}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
<ImageCarousel images={media} altText={name} isHovered={isHovered} />
|
||||||
<ImageCarousel images={media} altText={name} />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.productInfo}>
|
<div className={styles.productInfo}>
|
||||||
<h3 className={styles.productName}>{name}</h3>
|
<h3 className={styles.productName}>{name}</h3>
|
||||||
<p className={styles.productDescription}>{truncatedDesc}</p>
|
<p className={styles.productDescription}>{truncatedDesc}</p>
|
||||||
|
|
||||||
<div className={styles.priceContainer}>
|
<div className={styles.priceContainer}>
|
||||||
<div>
|
<div>
|
||||||
|
{isPriceZero(price_amount) ? (
|
||||||
|
<span className={styles.currentPrice}> {t("cart.pendingPriceTitle")}</span>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
<span className={styles.currentPrice}>{price_amount} m.</span>
|
<span className={styles.currentPrice}>{price_amount} m.</span>
|
||||||
{old_price_amount && (
|
{old_price_amount && (
|
||||||
<span className={styles.oldPrice}>{old_price_amount} m.</span>
|
<span className={styles.oldPrice}>{old_price_amount} m.</span>
|
||||||
)}
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.actions}>
|
<div className={styles.actions}>
|
||||||
{showFavoriteButton && (
|
{showFavoriteButton && (
|
||||||
<button
|
<button
|
||||||
@@ -261,6 +244,7 @@ const ProductCard = ({
|
|||||||
{localIsFavorite ? <IoMdHeart /> : <IoMdHeartEmpty />}
|
{localIsFavorite ? <IoMdHeart /> : <IoMdHeartEmpty />}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{showAddToCart && (
|
{showAddToCart && (
|
||||||
<>
|
<>
|
||||||
{localQuantity > 0 ? (
|
{localQuantity > 0 ? (
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
Info,
|
Info,
|
||||||
Edit,
|
Edit,
|
||||||
MapPin,
|
MapPin,
|
||||||
|
Store,
|
||||||
LogOut,
|
LogOut,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -24,12 +25,10 @@ import ru from "../../assets/ru.png";
|
|||||||
import en from "../../assets/en.png";
|
import en from "../../assets/en.png";
|
||||||
import { useAuth } from "../../context/authContext";
|
import { useAuth } from "../../context/authContext";
|
||||||
import { useGetProfileQuery } from "../../app/api/myProfileApi";
|
import { useGetProfileQuery } from "../../app/api/myProfileApi";
|
||||||
|
|
||||||
const ProfileMenu = () => {
|
const ProfileMenu = () => {
|
||||||
const [activeModal, setActiveModal] = useState(null);
|
const [activeModal, setActiveModal] = useState(null);
|
||||||
const { t, i18n } = useTranslation();
|
const { t, i18n } = useTranslation();
|
||||||
const { isAuthenticated, logout } = useAuth();
|
const { isAuthenticated, logout } = useAuth();
|
||||||
|
|
||||||
// Fetch profile data from API
|
// Fetch profile data from API
|
||||||
const { data: profileData, isLoading } = useGetProfileQuery(undefined, {
|
const { data: profileData, isLoading } = useGetProfileQuery(undefined, {
|
||||||
skip: !isAuthenticated, // Skip the API call if not authenticated
|
skip: !isAuthenticated, // Skip the API call if not authenticated
|
||||||
@@ -55,6 +54,11 @@ const ProfileMenu = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (item.action === "/panel") {
|
||||||
|
window.location.href = "/panel";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (item.action) {
|
if (item.action) {
|
||||||
setActiveModal(item.action);
|
setActiveModal(item.action);
|
||||||
}
|
}
|
||||||
@@ -84,6 +88,7 @@ const ProfileMenu = () => {
|
|||||||
{ icon: <Wallet />, text: t("profile.orders"), path: "/orders" },
|
{ icon: <Wallet />, text: t("profile.orders"), path: "/orders" },
|
||||||
{ icon: <Heart />, text: t("profile.favorites"), path: "/wishlist" },
|
{ icon: <Heart />, text: t("profile.favorites"), path: "/wishlist" },
|
||||||
{ icon: <Languages />, text: t("profile.language"), action: "language" },
|
{ icon: <Languages />, text: t("profile.language"), action: "language" },
|
||||||
|
{ icon: <Store />, text: t("profile.seller_panel"), action: "/panel" },
|
||||||
{
|
{
|
||||||
icon: <List />,
|
icon: <List />,
|
||||||
text: t("profile.delivery"),
|
text: t("profile.delivery"),
|
||||||
@@ -102,7 +107,9 @@ const ProfileMenu = () => {
|
|||||||
<User className={styles.userIcon} />
|
<User className={styles.userIcon} />
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.userInfo}>
|
<div className={styles.userInfo}>
|
||||||
<div className={styles.phoneNumber}>+993 {userData.phone_number}</div>
|
<div className={styles.phoneNumber}>
|
||||||
|
+993 {userData.phone_number}
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={handleEditProfile}
|
onClick={handleEditProfile}
|
||||||
className={styles.editProfileLink}
|
className={styles.editProfileLink}
|
||||||
@@ -192,6 +199,7 @@ const ProfileMenu = () => {
|
|||||||
{ icon: <Wallet />, text: t("profile.orders"), path: "/orders" },
|
{ icon: <Wallet />, text: t("profile.orders"), path: "/orders" },
|
||||||
{ icon: <Heart />, text: t("profile.favorites"), path: "/wishlist" },
|
{ icon: <Heart />, text: t("profile.favorites"), path: "/wishlist" },
|
||||||
{ icon: <Languages />, text: t("profile.language"), action: "language" },
|
{ icon: <Languages />, text: t("profile.language"), action: "language" },
|
||||||
|
{ icon: <Store />, text: t("profile.seller_panel"), action: "/panel" },
|
||||||
{
|
{
|
||||||
icon: <List />,
|
icon: <List />,
|
||||||
text: t("profile.delivery"),
|
text: t("profile.delivery"),
|
||||||
|
|||||||
241
src/components/StoryViewer/StoryViewer.jsx
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||||
|
import { X, ChevronLeft, ChevronRight } from 'lucide-react';
|
||||||
|
import styles from './StoryViewer.module.scss';
|
||||||
|
|
||||||
|
const STORY_DURATION = 6000;
|
||||||
|
|
||||||
|
const StoryViewer = ({ stories, onClose, initialIndex = 0, onStoryViewed }) => {
|
||||||
|
const [currentIndex, setCurrentIndex] = useState(initialIndex);
|
||||||
|
const [animClass, setAnimClass] = useState(styles.slideInFromRight);
|
||||||
|
const [progress, setProgress] = useState(0); // 0..1
|
||||||
|
|
||||||
|
const touchStartX = useRef(null);
|
||||||
|
const touchStartY = useRef(null);
|
||||||
|
const isPausedRef = useRef(false);
|
||||||
|
const rafRef = useRef(null);
|
||||||
|
const startTimeRef = useRef(null);
|
||||||
|
const elapsedRef = useRef(0); // ms elapsed before last pause
|
||||||
|
|
||||||
|
const isFirst = currentIndex === 0;
|
||||||
|
const isLast = currentIndex === stories.length - 1;
|
||||||
|
const currentStory = stories[currentIndex];
|
||||||
|
|
||||||
|
// Scroll lock
|
||||||
|
useEffect(() => {
|
||||||
|
const orig = document.body.style.overflow;
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
return () => { document.body.style.overflow = orig; };
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// ── Core timer: runs per-frame, advances progress, fires goNext ──
|
||||||
|
const startTimer = useCallback((onDone) => {
|
||||||
|
if (rafRef.current) cancelAnimationFrame(rafRef.current);
|
||||||
|
startTimeRef.current = Date.now();
|
||||||
|
elapsedRef.current = 0;
|
||||||
|
setProgress(0);
|
||||||
|
|
||||||
|
const tick = () => {
|
||||||
|
if (isPausedRef.current) {
|
||||||
|
// While paused keep scheduling but don't advance
|
||||||
|
rafRef.current = requestAnimationFrame(tick);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const now = Date.now();
|
||||||
|
const delta = now - startTimeRef.current;
|
||||||
|
startTimeRef.current = now;
|
||||||
|
elapsedRef.current += delta;
|
||||||
|
|
||||||
|
const p = Math.min(elapsedRef.current / STORY_DURATION, 1);
|
||||||
|
setProgress(p);
|
||||||
|
|
||||||
|
if (p >= 1) {
|
||||||
|
onDone();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
rafRef.current = requestAnimationFrame(tick);
|
||||||
|
};
|
||||||
|
|
||||||
|
rafRef.current = requestAnimationFrame(tick);
|
||||||
|
return () => cancelAnimationFrame(rafRef.current);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// ── Navigate ──
|
||||||
|
const goTo = useCallback((nextIndex, dir) => {
|
||||||
|
if (nextIndex < 0 || nextIndex >= stories.length) return;
|
||||||
|
if (rafRef.current) cancelAnimationFrame(rafRef.current);
|
||||||
|
setAnimClass(dir === 'next' ? styles.slideInFromRight : styles.slideInFromLeft);
|
||||||
|
setCurrentIndex(nextIndex);
|
||||||
|
}, [stories.length]);
|
||||||
|
|
||||||
|
const goToNext = useCallback(() => {
|
||||||
|
if (!isLast) goTo(currentIndex + 1, 'next');
|
||||||
|
else onClose();
|
||||||
|
}, [currentIndex, isLast, goTo, onClose]);
|
||||||
|
|
||||||
|
const goToPrevious = useCallback(() => {
|
||||||
|
if (!isFirst) goTo(currentIndex - 1, 'prev');
|
||||||
|
}, [currentIndex, isFirst, goTo]);
|
||||||
|
|
||||||
|
// ── Start timer when index changes ──
|
||||||
|
useEffect(() => {
|
||||||
|
if (onStoryViewed) onStoryViewed(currentIndex);
|
||||||
|
|
||||||
|
const cancel = startTimer(() => {
|
||||||
|
if (currentIndex < stories.length - 1) {
|
||||||
|
setAnimClass(styles.slideInFromRight);
|
||||||
|
setCurrentIndex((i) => i + 1);
|
||||||
|
} else {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return cancel;
|
||||||
|
}, [currentIndex, stories.length]); // NO isPaused here — handled by ref
|
||||||
|
|
||||||
|
// ── Keyboard ──
|
||||||
|
useEffect(() => {
|
||||||
|
const onKey = (e) => {
|
||||||
|
if (e.key === 'ArrowLeft') goToPrevious();
|
||||||
|
if (e.key === 'ArrowRight') goToNext();
|
||||||
|
if (e.key === 'Escape') onClose();
|
||||||
|
};
|
||||||
|
window.addEventListener('keydown', onKey);
|
||||||
|
return () => window.removeEventListener('keydown', onKey);
|
||||||
|
}, [goToPrevious, goToNext, onClose]);
|
||||||
|
|
||||||
|
// ── Touch swipe ──
|
||||||
|
const handleTouchStart = (e) => {
|
||||||
|
touchStartX.current = e.touches[0].clientX;
|
||||||
|
touchStartY.current = e.touches[0].clientY;
|
||||||
|
};
|
||||||
|
const handleTouchEnd = (e) => {
|
||||||
|
if (touchStartX.current === null) return;
|
||||||
|
const dx = e.changedTouches[0].clientX - touchStartX.current;
|
||||||
|
const dy = e.changedTouches[0].clientY - touchStartY.current;
|
||||||
|
touchStartX.current = null;
|
||||||
|
if (Math.abs(dx) < 40 || Math.abs(dx) < Math.abs(dy)) return;
|
||||||
|
if (dx < 0) goToNext();
|
||||||
|
else goToPrevious();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseEnter = () => {
|
||||||
|
isPausedRef.current = true;
|
||||||
|
// Snapshot elapsed so we resume correctly
|
||||||
|
if (startTimeRef.current !== null) {
|
||||||
|
elapsedRef.current += Date.now() - startTimeRef.current;
|
||||||
|
startTimeRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseLeave = () => {
|
||||||
|
isPausedRef.current = false;
|
||||||
|
startTimeRef.current = Date.now(); // reset delta start
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!currentStory) return null;
|
||||||
|
|
||||||
|
const getTimeAgo = (dateString) => {
|
||||||
|
if (!dateString) return '';
|
||||||
|
const diff = Math.floor((Date.now() - new Date(dateString)) / 1000);
|
||||||
|
if (diff < 60) return 'şimdi';
|
||||||
|
if (diff < 3600) return `${Math.floor(diff / 60)}d`;
|
||||||
|
if (diff < 86400) return `${Math.floor(diff / 3600)}s`;
|
||||||
|
return `${Math.floor(diff / 86400)}g`;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.overlay} onClick={onClose}>
|
||||||
|
{!isFirst && (
|
||||||
|
<button
|
||||||
|
className={`${styles.outerNav} ${styles.outerNavLeft}`}
|
||||||
|
onClick={(e) => { e.stopPropagation(); goToPrevious(); }}
|
||||||
|
aria-label="Önceki"
|
||||||
|
>
|
||||||
|
<ChevronLeft size={28} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={styles.container}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
onMouseEnter={handleMouseEnter}
|
||||||
|
onMouseLeave={handleMouseLeave}
|
||||||
|
onTouchStart={handleTouchStart}
|
||||||
|
onTouchEnd={handleTouchEnd}
|
||||||
|
>
|
||||||
|
{/* Progress bars — driven by JS progress state */}
|
||||||
|
<div className={styles.progressBars}>
|
||||||
|
{stories.map((_, idx) => (
|
||||||
|
<div key={idx} className={styles.track}>
|
||||||
|
{idx < currentIndex && (
|
||||||
|
<div className={styles.bar} style={{ width: '100%' }} />
|
||||||
|
)}
|
||||||
|
{idx === currentIndex && (
|
||||||
|
<div className={styles.bar} style={{ width: `${progress * 100}%` }} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
|
<div className={styles.header}>
|
||||||
|
<div className={styles.identity}>
|
||||||
|
<div className={styles.avatar}>
|
||||||
|
<img src={currentStory.thumbnail || currentStory.photo} alt={currentStory.title} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className={styles.name}>{currentStory.title}</p>
|
||||||
|
{currentStory.createdAt && (
|
||||||
|
<p className={styles.time}>{getTimeAgo(currentStory.createdAt)}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button className={styles.close} onClick={onClose} aria-label="Kapat">
|
||||||
|
<X size={20} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Image */}
|
||||||
|
<div className={styles.media}>
|
||||||
|
<img
|
||||||
|
key={currentIndex}
|
||||||
|
src={currentStory.photo || currentStory.image}
|
||||||
|
alt={currentStory.title}
|
||||||
|
className={animClass}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className={`${styles.tap} ${styles.tapLeft}`}
|
||||||
|
onClick={goToPrevious}
|
||||||
|
disabled={isFirst}
|
||||||
|
aria-label="Önceki"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className={`${styles.tap} ${styles.tapRight}`}
|
||||||
|
onClick={goToNext}
|
||||||
|
aria-label="Sonraki"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{(currentStory.description || currentStory.caption) && (
|
||||||
|
<div className={styles.footer}>
|
||||||
|
<p className={styles.caption}>
|
||||||
|
{currentStory.description || currentStory.caption}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!isLast && (
|
||||||
|
<button
|
||||||
|
className={`${styles.outerNav} ${styles.outerNavRight}`}
|
||||||
|
onClick={(e) => { e.stopPropagation(); goToNext(); }}
|
||||||
|
aria-label="Sonraki"
|
||||||
|
>
|
||||||
|
<ChevronRight size={28} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StoryViewer;
|
||||||
245
src/components/StoryViewer/StoryViewer.module.scss
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
.overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.92);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
animation: fadeIn 0.2s ease;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 420px;
|
||||||
|
height: 90vh;
|
||||||
|
max-height: 780px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: #111;
|
||||||
|
border-radius: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
touch-action: pan-y;
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
max-width: 100vw;
|
||||||
|
height: 100dvh;
|
||||||
|
max-height: none;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Outer nav ── */
|
||||||
|
.outerNav {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(255, 255, 255, 0.12);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||||
|
color: #fff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
z-index: 10;
|
||||||
|
transition: background 0.15s, transform 0.15s;
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.22);
|
||||||
|
transform: translateY(-50%) scale(1.08);
|
||||||
|
}
|
||||||
|
&:active { transform: translateY(-50%) scale(0.95); }
|
||||||
|
|
||||||
|
@media (max-width: 600px) { display: none; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.outerNavLeft {
|
||||||
|
left: calc(50% - 210px - 60px);
|
||||||
|
@media (max-width: 700px) { left: 8px; }
|
||||||
|
}
|
||||||
|
.outerNavRight {
|
||||||
|
right: calc(50% - 210px - 60px);
|
||||||
|
@media (max-width: 700px) { right: 8px; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Progress bars — JS driven, no CSS animation ── */
|
||||||
|
.progressBars {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
display: flex;
|
||||||
|
gap: 3px;
|
||||||
|
padding: 10px 10px 0;
|
||||||
|
z-index: 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track {
|
||||||
|
flex: 1;
|
||||||
|
height: 2px;
|
||||||
|
background: rgba(255, 255, 255, 0.25);
|
||||||
|
border-radius: 2px;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Single bar element — width set via inline style */
|
||||||
|
.bar {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 100%;
|
||||||
|
background: #e53935;
|
||||||
|
border-radius: 2px;
|
||||||
|
/* No transition — rAF updates are fast enough */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Header ── */
|
||||||
|
.header {
|
||||||
|
position: absolute;
|
||||||
|
top: 22px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 10px 14px;
|
||||||
|
z-index: 20;
|
||||||
|
background: linear-gradient(180deg, rgba(0,0,0,0.55) 0%, transparent 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.identity {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 1.5px solid rgba(255, 255, 255, 0.65);
|
||||||
|
overflow: hidden;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time {
|
||||||
|
color: rgba(255, 255, 255, 0.55);
|
||||||
|
font-size: 11px;
|
||||||
|
margin: 0;
|
||||||
|
margin-top: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: color 0.15s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
&:hover { color: #fff; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Media ── */
|
||||||
|
.media {
|
||||||
|
flex: 1;
|
||||||
|
position: relative;
|
||||||
|
background: #000;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slideInFromRight {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
animation: slideInRight 0.35s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slideInFromLeft {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
animation: slideInLeft 0.35s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tap zones */
|
||||||
|
.tap {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 40%;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
outline: none;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
z-index: 10;
|
||||||
|
|
||||||
|
&:disabled { pointer-events: none; }
|
||||||
|
}
|
||||||
|
.tapLeft { left: 0; }
|
||||||
|
.tapRight { right: 0; }
|
||||||
|
|
||||||
|
/* ── Footer ── */
|
||||||
|
.footer {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
padding: 28px 16px 18px;
|
||||||
|
background: linear-gradient(0deg, rgba(0,0,0,0.72) 0%, transparent 100%);
|
||||||
|
z-index: 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.caption {
|
||||||
|
color: rgba(255, 255, 255, 0.88);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Keyframes ── */
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; }
|
||||||
|
to { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideInRight {
|
||||||
|
from { opacity: 0; transform: translateX(5%) scale(0.98); }
|
||||||
|
to { opacity: 1; transform: translateX(0) scale(1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideInLeft {
|
||||||
|
from { opacity: 0; transform: translateX(-5%) scale(0.98); }
|
||||||
|
to { opacity: 1; transform: translateX(0) scale(1); }
|
||||||
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
navbar: {
|
navbar: {
|
||||||
category: "Categories",
|
category: "Categories",
|
||||||
login: "Login",
|
login: "Login",
|
||||||
signUp: "Sign Up",
|
signUp: "Sign Up",
|
||||||
brands: "Brands",
|
brands: "Brands",
|
||||||
|
stores: "Stores",
|
||||||
search: "Search by product name...",
|
search: "Search by product name...",
|
||||||
cart: "Cart",
|
cart: "Cart",
|
||||||
home: "Home",
|
home: "Home",
|
||||||
@@ -12,6 +14,14 @@ export default {
|
|||||||
ru: "Русский",
|
ru: "Русский",
|
||||||
en: "English",
|
en: "English",
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
flashSales: {
|
||||||
|
flash_sale: "FLASH SALE",
|
||||||
|
ends_in: "Ends in:",
|
||||||
|
day: "day",
|
||||||
|
hour: "hr",
|
||||||
|
minute: "min",
|
||||||
|
second: "sec",
|
||||||
},
|
},
|
||||||
cart: {
|
cart: {
|
||||||
basket: "Basket",
|
basket: "Basket",
|
||||||
@@ -29,6 +39,9 @@ export default {
|
|||||||
emptyCartTitle: "Your cart is empty",
|
emptyCartTitle: "Your cart is empty",
|
||||||
emptyCartMessage: "Looks like you haven't added any items to your cart yet",
|
emptyCartMessage: "Looks like you haven't added any items to your cart yet",
|
||||||
continueShopping: "Continue Shopping",
|
continueShopping: "Continue Shopping",
|
||||||
|
pendingPriceTitle: "Price pending",
|
||||||
|
pendingPriceDesc: "The price of one or more items in this order has not yet been determined. Our operator will contact you to provide additional information.",
|
||||||
|
pendingPriceTooltipDesc: "The price of this item in the order has not been determined. The operator will call you and provide additional information."
|
||||||
},
|
},
|
||||||
checkout: {
|
checkout: {
|
||||||
paymentMethod: "Payment Method",
|
paymentMethod: "Payment Method",
|
||||||
@@ -129,6 +142,8 @@ export default {
|
|||||||
verify: "Verify",
|
verify: "Verify",
|
||||||
name: "Name",
|
name: "Name",
|
||||||
address: "Address",
|
address: "Address",
|
||||||
|
seller_panel: "Seller Panel",
|
||||||
|
|
||||||
},
|
},
|
||||||
order: {
|
order: {
|
||||||
orderDate: "Order Date",
|
orderDate: "Order Date",
|
||||||
@@ -172,11 +187,25 @@ export default {
|
|||||||
price: "Price",
|
price: "Price",
|
||||||
minPrice: "Min Price",
|
minPrice: "Min Price",
|
||||||
maxPrice: "Max Price",
|
maxPrice: "Max Price",
|
||||||
|
priceHighToLow: "From expensive to cheap",
|
||||||
|
priceLowToHigh: "From cheap to expensive",
|
||||||
|
priceRange: "Price Range",
|
||||||
|
under50: "Under 50m",
|
||||||
|
under100: "Under 100m",
|
||||||
|
from50to200: "50 - 200",
|
||||||
|
from200to500: "200 - 500",
|
||||||
|
from500to1000: "500 - 1000",
|
||||||
|
over1000: "Over 1000m",
|
||||||
|
sortBy: "Sort By",
|
||||||
},
|
},
|
||||||
product: {
|
product: {
|
||||||
productCode: "Product code",
|
productCode: "Product code",
|
||||||
barCode: "Barcode",
|
barCode: "Barcode",
|
||||||
similarProducts: "Similar Products",
|
similarProducts: "Similar Products",
|
||||||
|
description: "Product description",
|
||||||
|
price: "Price",
|
||||||
|
readMore: "Read more...",
|
||||||
|
readLess: "Show less",
|
||||||
},
|
},
|
||||||
wishtList: {
|
wishtList: {
|
||||||
likedProducts: "Favorites",
|
likedProducts: "Favorites",
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
navbar: {
|
navbar: {
|
||||||
category: "Категории",
|
category: "Категории",
|
||||||
login: "Войти",
|
login: "Войти",
|
||||||
signUp: "Регистрация",
|
signUp: "Регистрация",
|
||||||
brands: "Бренды",
|
brands: "Бренды",
|
||||||
|
stores: "Магазины",
|
||||||
search: "Поиск по названию товара...",
|
search: "Поиск по названию товара...",
|
||||||
cart: "Корзина",
|
cart: "Корзина",
|
||||||
home: "Главная",
|
home: "Главная",
|
||||||
@@ -12,6 +14,14 @@ export default {
|
|||||||
ru: "Русский",
|
ru: "Русский",
|
||||||
en: "English",
|
en: "English",
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
flashSales: {
|
||||||
|
flash_sale: "ФЛЭШ-РАСПРОДАЖА",
|
||||||
|
ends_in: "До конца:",
|
||||||
|
day: "дн.",
|
||||||
|
hour: "ч.",
|
||||||
|
minute: "мин.",
|
||||||
|
second: "сек.",
|
||||||
},
|
},
|
||||||
cart: {
|
cart: {
|
||||||
basket: "Корзина",
|
basket: "Корзина",
|
||||||
@@ -29,6 +39,9 @@ export default {
|
|||||||
emptyCartTitle: "Ваша корзина пуста",
|
emptyCartTitle: "Ваша корзина пуста",
|
||||||
emptyCartMessage: "Похоже, вы еще не добавили ни одного товара в корзину",
|
emptyCartMessage: "Похоже, вы еще не добавили ни одного товара в корзину",
|
||||||
continueShopping: "Продолжить покупки",
|
continueShopping: "Продолжить покупки",
|
||||||
|
pendingPriceTitle: "Цена уточняется",
|
||||||
|
pendingPriceDesc: "Цена на один или несколько товаров в этом заказе еще не определена. Наш оператор свяжется с вами для предоставления дополнительной информации.",
|
||||||
|
pendingPriceTooltipDesc: "Цена на этот товар в заказе не определена. Оператор позвонит вам и предоставит дополнительную информацию."
|
||||||
},
|
},
|
||||||
checkout: {
|
checkout: {
|
||||||
paymentMethod: "Способ оплаты",
|
paymentMethod: "Способ оплаты",
|
||||||
@@ -126,6 +139,7 @@ export default {
|
|||||||
name: "Имя",
|
name: "Имя",
|
||||||
address: "Address",
|
address: "Address",
|
||||||
lastname: "Фамилия",
|
lastname: "Фамилия",
|
||||||
|
seller_panel: "Панель продавца",
|
||||||
},
|
},
|
||||||
order: {
|
order: {
|
||||||
orderDate: "Дата заказа",
|
orderDate: "Дата заказа",
|
||||||
@@ -167,13 +181,27 @@ export default {
|
|||||||
From_expensive_to_cheap: "От дорогих к дешевым",
|
From_expensive_to_cheap: "От дорогих к дешевым",
|
||||||
From_cheap_to_expensive: "От дешевых к дорогим",
|
From_cheap_to_expensive: "От дешевых к дорогим",
|
||||||
price: "Цена",
|
price: "Цена",
|
||||||
minPrice: "Минимальная цена",
|
minPrice: "Мин цена",
|
||||||
maxPrice: "Максимальная цена",
|
maxPrice: "Макс цена",
|
||||||
|
priceHighToLow: "От дорогих к дешевым",
|
||||||
|
priceLowToHigh: "От дешевых к дорогим",
|
||||||
|
priceRange: "Диапазон цен",
|
||||||
|
under50: "До 50m",
|
||||||
|
under100: "До 100m",
|
||||||
|
from50to200: "50 - 200",
|
||||||
|
from200to500: "200 - 500",
|
||||||
|
from500to1000: "500 - 1000",
|
||||||
|
over1000: "Более 1000m",
|
||||||
|
sortBy: "Сортировать по",
|
||||||
},
|
},
|
||||||
product: {
|
product: {
|
||||||
productCode: "Код товара",
|
productCode: "Код товара",
|
||||||
barCode: "Штрих-код",
|
barCode: "Штрих-код",
|
||||||
similarProducts: "Похожие товары",
|
similarProducts: "Похожие товары",
|
||||||
|
description: "Описание товара",
|
||||||
|
price: "Цена",
|
||||||
|
readMore: "Читать далее...",
|
||||||
|
readLess: "Свернуть",
|
||||||
},
|
},
|
||||||
wishtList: {
|
wishtList: {
|
||||||
likedProducts: "Избранные",
|
likedProducts: "Избранные",
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
navbar: {
|
navbar: {
|
||||||
category: "Kategoriýalar",
|
category: "Kategoriýalar",
|
||||||
login: "Giriş",
|
login: "Giriş",
|
||||||
signUp: "Agza bolmak",
|
signUp: "Agza bolmak",
|
||||||
brands: "Brendler",
|
brands: "Brendler",
|
||||||
|
stores: "Dükanlar",
|
||||||
search: "Haryt ady boýunça gözleg...",
|
search: "Haryt ady boýunça gözleg...",
|
||||||
cart: "Sebet",
|
cart: "Sebet",
|
||||||
home: "Baş sahypa",
|
home: "Baş sahypa",
|
||||||
@@ -12,6 +14,14 @@ export default {
|
|||||||
ru: "Русский",
|
ru: "Русский",
|
||||||
en: "English",
|
en: "English",
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
flashSales: {
|
||||||
|
flash_sale: "GYSGA WAGTLYK ARZANLADYŞ",
|
||||||
|
ends_in: "Gutarýança:",
|
||||||
|
day: "gün",
|
||||||
|
hour: "sag",
|
||||||
|
minute: "min",
|
||||||
|
second: "sek",
|
||||||
},
|
},
|
||||||
cart: {
|
cart: {
|
||||||
basket: "Sebet",
|
basket: "Sebet",
|
||||||
@@ -29,6 +39,9 @@ export default {
|
|||||||
emptyCartTitle: "Sebediňiz boş",
|
emptyCartTitle: "Sebediňiz boş",
|
||||||
emptyCartMessage: "Sebediňize entek hiç zat goşmadyňyz.",
|
emptyCartMessage: "Sebediňize entek hiç zat goşmadyňyz.",
|
||||||
continueShopping: "Söwda etmegi dowam etdiriň",
|
continueShopping: "Söwda etmegi dowam etdiriň",
|
||||||
|
pendingPriceTitle: "Bahasyny anyklamaly",
|
||||||
|
pendingPriceDesc: "Bu sargytdaky bir ýa-da birnäçe harydyň bahasy entek kesgitlenmedik. Operatorymyz siziň bilen habarlaşyp, goşmaça maglumat berer.",
|
||||||
|
pendingPriceTooltipDesc: "Bu sargytdaky harydyň bahasy kesgitlenmedik. Operator size jaň edip goşmaça maglumat berer."
|
||||||
},
|
},
|
||||||
checkout: {
|
checkout: {
|
||||||
paymentMethod: "Töleg görnüşi",
|
paymentMethod: "Töleg görnüşi",
|
||||||
@@ -129,6 +142,7 @@ export default {
|
|||||||
name: "Ady",
|
name: "Ady",
|
||||||
address: "Salgy",
|
address: "Salgy",
|
||||||
lastname: "Familýaňyz",
|
lastname: "Familýaňyz",
|
||||||
|
seller_panel: "Satyjy paneli",
|
||||||
},
|
},
|
||||||
order: {
|
order: {
|
||||||
orderDate: "Sargyt senesi",
|
orderDate: "Sargyt senesi",
|
||||||
@@ -170,13 +184,27 @@ export default {
|
|||||||
From_expensive_to_cheap: "Gymmatdan arzana",
|
From_expensive_to_cheap: "Gymmatdan arzana",
|
||||||
From_cheap_to_expensive: "Arzandan gymmada",
|
From_cheap_to_expensive: "Arzandan gymmada",
|
||||||
price: "Bahasy",
|
price: "Bahasy",
|
||||||
maxPrice: "Maksimum baha",
|
maxPrice: "Maks baha",
|
||||||
minPrice: "Minimum baha",
|
minPrice: "Min baha",
|
||||||
|
priceHighToLow: "Gymmatdan arzana",
|
||||||
|
priceLowToHigh: "Arzandan gymmada",
|
||||||
|
priceRange: "Baha diapazony",
|
||||||
|
under50: "50m aşagynda",
|
||||||
|
under100: "100m aşagynda",
|
||||||
|
from50to200: "50 - 200",
|
||||||
|
from200to500: "200 - 500",
|
||||||
|
from500to1000: "500 - 1000",
|
||||||
|
over1000: "1000m dan ýokary",
|
||||||
|
sortBy: "Tertiplemek",
|
||||||
},
|
},
|
||||||
product: {
|
product: {
|
||||||
productCode: "Haryt kody",
|
productCode: "Haryt kody",
|
||||||
barCode: "Çyzgyç kod",
|
barCode: "Çyzgyç kod",
|
||||||
similarProducts: "Meňzeş harytlar",
|
similarProducts: "Meňzeş harytlar",
|
||||||
|
description: "Haryt barada düşündiriş",
|
||||||
|
price: "Bahasy",
|
||||||
|
readMore: "Giňişleýin oka...",
|
||||||
|
readLess: "Gysgaltmak",
|
||||||
},
|
},
|
||||||
wishtList: {
|
wishtList: {
|
||||||
likedProducts: "Halanlarym",
|
likedProducts: "Halanlarym",
|
||||||
|
|||||||
651
src/pages/CarconfiguratorAdmin/Adminpage.module.scss
Normal file
@@ -0,0 +1,651 @@
|
|||||||
|
// ── Variables ─────────────────────────────────────────────────
|
||||||
|
$orange: #f26522;
|
||||||
|
$orange-dark: #d4551a;
|
||||||
|
$bg: #f0f0f0;
|
||||||
|
$white: #ffffff;
|
||||||
|
$border: #e0e0e0;
|
||||||
|
$text: #1a1a1a;
|
||||||
|
$muted: #888;
|
||||||
|
$danger: #e53935;
|
||||||
|
$success: #2e7d32;
|
||||||
|
$radius-sm: 6px;
|
||||||
|
$radius-md: 10px;
|
||||||
|
$radius-lg: 16px;
|
||||||
|
$transition: 0.2s ease;
|
||||||
|
|
||||||
|
// ── Login ─────────────────────────────────────────────────────
|
||||||
|
.loginWrapper {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: linear-gradient(135deg, #1a1a1a 0%, #2d2d2d 100%);
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loginCard {
|
||||||
|
background: $white;
|
||||||
|
border-radius: $radius-lg;
|
||||||
|
padding: 48px 40px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 380px;
|
||||||
|
box-shadow: 0 32px 80px rgba(0, 0, 0, 0.4);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loginLogo { font-size: 3rem; margin-bottom: 16px; }
|
||||||
|
|
||||||
|
.loginTitle {
|
||||||
|
font-family: "Segoe UI", sans-serif;
|
||||||
|
font-size: 1.3rem;
|
||||||
|
font-weight: 800;
|
||||||
|
color: $text;
|
||||||
|
margin: 0 0 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loginSub {
|
||||||
|
font-family: "Segoe UI", sans-serif;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
color: $muted;
|
||||||
|
margin: 0 0 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loginForm { display: flex; flex-direction: column; gap: 12px; }
|
||||||
|
|
||||||
|
.loginInput {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border: 2px solid $border;
|
||||||
|
border-radius: $radius-md;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-family: "Segoe UI", sans-serif;
|
||||||
|
outline: none;
|
||||||
|
transition: border-color $transition;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
&:focus { border-color: $orange; }
|
||||||
|
&.inputError { border-color: $danger; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.errorMsg {
|
||||||
|
font-family: "Segoe UI", sans-serif;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
color: $danger;
|
||||||
|
margin: -4px 0 0;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loginBtn {
|
||||||
|
padding: 12px;
|
||||||
|
background: $orange;
|
||||||
|
color: $white;
|
||||||
|
border: none;
|
||||||
|
border-radius: $radius-md;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-family: "Segoe UI", sans-serif;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background $transition;
|
||||||
|
|
||||||
|
&:hover { background: $orange-dark; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shake {
|
||||||
|
0%, 100% { transform: translateX(0); }
|
||||||
|
20% { transform: translateX(-8px); }
|
||||||
|
40% { transform: translateX(8px); }
|
||||||
|
60% { transform: translateX(-6px); }
|
||||||
|
80% { transform: translateX(6px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.shake { animation: shake 0.45s ease; }
|
||||||
|
|
||||||
|
// ── Loading ───────────────────────────────────────────────────
|
||||||
|
.loadingScreen {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-family: "Segoe UI", sans-serif;
|
||||||
|
font-size: 1rem;
|
||||||
|
color: $muted;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Admin layout ──────────────────────────────────────────────
|
||||||
|
.adminWrapper {
|
||||||
|
font-family: "Segoe UI", sans-serif;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: $bg;
|
||||||
|
color: $text;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding-bottom: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Header ────────────────────────────────────────────────────
|
||||||
|
.adminHeader {
|
||||||
|
background: $white;
|
||||||
|
border-bottom: 1px solid $border;
|
||||||
|
padding: 16px 28px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 100;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,.06);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.adminHeaderLeft {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.adminHeaderIcon { font-size: 1.8rem; }
|
||||||
|
|
||||||
|
.adminHeaderTitle {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 800;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.adminHeaderSub {
|
||||||
|
font-size: 0.72rem;
|
||||||
|
color: $muted;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.adminHeaderRight {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.savedBadge {
|
||||||
|
background: lighten($success, 58%);
|
||||||
|
color: $success;
|
||||||
|
border: 1px solid lighten($success, 40%);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 4px 12px;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnSaveHeader {
|
||||||
|
padding: 8px 18px;
|
||||||
|
background: $orange;
|
||||||
|
color: $white;
|
||||||
|
border: none;
|
||||||
|
border-radius: $radius-sm;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background $transition;
|
||||||
|
|
||||||
|
&:hover { background: $orange-dark; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnLogout {
|
||||||
|
padding: 8px 18px;
|
||||||
|
background: $white;
|
||||||
|
color: $muted;
|
||||||
|
border: 1.5px solid $border;
|
||||||
|
border-radius: $radius-sm;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color $transition, border-color $transition;
|
||||||
|
|
||||||
|
&:hover { color: $danger; border-color: $danger; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Tab bar ───────────────────────────────────────────────────
|
||||||
|
.tabBar {
|
||||||
|
display: flex;
|
||||||
|
gap: 0;
|
||||||
|
background: $white;
|
||||||
|
border-bottom: 2px solid $border;
|
||||||
|
padding: 0 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabBtn {
|
||||||
|
padding: 12px 24px;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: $muted;
|
||||||
|
cursor: pointer;
|
||||||
|
border-bottom: 3px solid transparent;
|
||||||
|
margin-bottom: -2px;
|
||||||
|
transition: color $transition, border-color $transition;
|
||||||
|
|
||||||
|
&:hover { color: $orange; }
|
||||||
|
|
||||||
|
&.tabActive {
|
||||||
|
color: $orange;
|
||||||
|
border-bottom-color: $orange;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Products tab body ─────────────────────────────────────────
|
||||||
|
.adminBody {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 220px 1fr;
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
@media (max-width: 700px) { grid-template-columns: 1fr; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Sidebar ───────────────────────────────────────────────────
|
||||||
|
.sidebar {
|
||||||
|
background: $white;
|
||||||
|
border-right: 1px solid $border;
|
||||||
|
padding: 20px 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
min-height: calc(100vh - 200px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebarLabel {
|
||||||
|
font-size: 0.65rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
color: $muted;
|
||||||
|
margin: 0 0 8px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebarItem {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
padding: 9px 12px;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
border-radius: $radius-sm;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: $text;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background $transition;
|
||||||
|
|
||||||
|
&:hover { background: $bg; }
|
||||||
|
|
||||||
|
&.sidebarActive {
|
||||||
|
background: lighten($orange, 44%);
|
||||||
|
color: $orange;
|
||||||
|
font-weight: 700;
|
||||||
|
border-left: 3px solid $orange;
|
||||||
|
padding-left: 9px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Content ───────────────────────────────────────────────────
|
||||||
|
.content {
|
||||||
|
padding: 28px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Field ─────────────────────────────────────────────────────
|
||||||
|
.fieldRow {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fieldLabel {
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
color: $muted;
|
||||||
|
white-space: nowrap;
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fieldInput {
|
||||||
|
flex: 1;
|
||||||
|
max-width: 360px;
|
||||||
|
padding: 9px 14px;
|
||||||
|
border: 1.5px solid $border;
|
||||||
|
border-radius: $radius-sm;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
font-family: "Segoe UI", sans-serif;
|
||||||
|
outline: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: border-color $transition;
|
||||||
|
|
||||||
|
&:focus { border-color: $orange; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Body type pills row ───────────────────────────────────────
|
||||||
|
.bodyTypeRow {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bodyTypePills {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bodyPill {
|
||||||
|
padding: 6px 14px;
|
||||||
|
border-radius: 20px;
|
||||||
|
border: 1.5px solid $border;
|
||||||
|
background: $bg;
|
||||||
|
color: $text;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color $transition, background $transition, color $transition;
|
||||||
|
|
||||||
|
&:hover { border-color: $orange; }
|
||||||
|
|
||||||
|
&.bodyPillActive {
|
||||||
|
border-color: $orange;
|
||||||
|
background: lighten($orange, 44%);
|
||||||
|
color: $orange;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bodyPillIcon {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
margin-right: 8px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bodyPillEmoji {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Package tabs ──────────────────────────────────────────────
|
||||||
|
.pkgTabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
border-bottom: 2px solid $border;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pkgTab {
|
||||||
|
padding: 8px 20px;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 700;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
color: $muted;
|
||||||
|
cursor: pointer;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
margin-bottom: -2px;
|
||||||
|
transition: color $transition, border-color $transition;
|
||||||
|
|
||||||
|
&:hover { color: $orange; }
|
||||||
|
&.pkgActive { color: $orange; border-bottom-color: $orange; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Table ─────────────────────────────────────────────────────
|
||||||
|
.tableWrapper {
|
||||||
|
background: $white;
|
||||||
|
border-radius: $radius-md;
|
||||||
|
border: 1px solid $border;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table { width: 100%; border-collapse: collapse; }
|
||||||
|
|
||||||
|
.th {
|
||||||
|
text-align: left;
|
||||||
|
font-size: 0.68rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.07em;
|
||||||
|
color: $muted;
|
||||||
|
padding: 10px 14px;
|
||||||
|
background: $bg;
|
||||||
|
border-bottom: 2px solid $border;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thNum { width: 110px; }
|
||||||
|
|
||||||
|
.tr {
|
||||||
|
&:not(:last-child) { border-bottom: 1px solid $border; }
|
||||||
|
&:hover { background: rgba($orange, 0.025); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.td { padding: 8px 10px; font-size: 0.82rem; vertical-align: middle; }
|
||||||
|
|
||||||
|
.tdTotal { font-weight: 700; color: $orange-dark; white-space: nowrap; }
|
||||||
|
|
||||||
|
.cellInput {
|
||||||
|
width: 100%;
|
||||||
|
padding: 7px 10px;
|
||||||
|
border: 1.5px solid $border;
|
||||||
|
border-radius: $radius-sm;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
font-family: "Segoe UI", sans-serif;
|
||||||
|
outline: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: border-color $transition;
|
||||||
|
|
||||||
|
&:focus { border-color: $orange; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.cellNum { text-align: right; }
|
||||||
|
|
||||||
|
.emptyRow {
|
||||||
|
text-align: center;
|
||||||
|
padding: 24px;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
color: $muted;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnAdd {
|
||||||
|
align-self: flex-start;
|
||||||
|
background: none;
|
||||||
|
border: 2px dashed $orange;
|
||||||
|
color: $orange;
|
||||||
|
border-radius: $radius-md;
|
||||||
|
padding: 9px 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
font-family: "Segoe UI", sans-serif;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background $transition;
|
||||||
|
|
||||||
|
&:hover { background: lighten($orange, 46%); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnDel {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: $danger;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: $radius-sm;
|
||||||
|
transition: background $transition;
|
||||||
|
|
||||||
|
&:hover { background: lighten($danger, 46%); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Body Types tab ────────────────────────────────────────────
|
||||||
|
.bodyTypesPage {
|
||||||
|
padding: 28px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bodyTypesHeader { display: flex; flex-direction: column; gap: 6px; }
|
||||||
|
|
||||||
|
.bodyTypesTitle {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 800;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bodyTypesSub {
|
||||||
|
font-size: 0.82rem;
|
||||||
|
color: $muted;
|
||||||
|
margin: 0;
|
||||||
|
max-width: 540px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bodyTypesGrid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bodyTypeCard {
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.imageUploader {
|
||||||
|
position: relative;
|
||||||
|
background: #f9f9f9;
|
||||||
|
padding: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iconPreview {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
font-size: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploadLabel {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 8px;
|
||||||
|
right: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
color: white;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.imageUploader:hover .uploadLabel {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── FIX: bodyTypeCardBottom — input görünür + delete butonu hizalı ──
|
||||||
|
.bodyTypeCardBottom {
|
||||||
|
padding: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
background: white;
|
||||||
|
|
||||||
|
// Global .fieldInput'taki max-width:360px'i burada ezip
|
||||||
|
// flex container içinde düzgün genişlemesini sağlıyoruz
|
||||||
|
.fieldInput {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0; // flex shrink için zorunlu
|
||||||
|
max-width: none; // ← Ana sorun buydu
|
||||||
|
}
|
||||||
|
|
||||||
|
// Silme butonu sabit genişlikte, flex'ten etkilenmesin
|
||||||
|
.btnDel {
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bodyTypeAddCard {
|
||||||
|
background: none;
|
||||||
|
border: 2px dashed $border;
|
||||||
|
border-radius: $radius-md;
|
||||||
|
padding: 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: $muted;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: border-color $transition, color $transition;
|
||||||
|
min-height: 100px;
|
||||||
|
|
||||||
|
span:first-child { font-size: 1.8rem; }
|
||||||
|
|
||||||
|
&:hover { border-color: $orange; color: $orange; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Save hint ─────────────────────────────────────────────────
|
||||||
|
.saveHint {
|
||||||
|
background: lighten($orange, 46%);
|
||||||
|
border: 1px solid lighten($orange, 30%);
|
||||||
|
border-radius: $radius-md;
|
||||||
|
padding: 14px 18px;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
color: darken($orange, 10%);
|
||||||
|
max-width: 600px;
|
||||||
|
|
||||||
|
p { margin: 0; line-height: 1.6; }
|
||||||
|
code {
|
||||||
|
background: rgba($orange, 0.12);
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Floating save button ──────────────────────────────────────
|
||||||
|
.floatSave {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 24px;
|
||||||
|
right: 24px;
|
||||||
|
z-index: 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnSaveFloat {
|
||||||
|
padding: 12px 24px;
|
||||||
|
background: $orange;
|
||||||
|
color: $white;
|
||||||
|
border: none;
|
||||||
|
border-radius: $radius-lg;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
font-family: "Segoe UI", sans-serif;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 6px 20px rgba($orange, 0.45);
|
||||||
|
transition: background $transition, transform $transition;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: $orange-dark;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
}
|
||||||
435
src/pages/CarconfiguratorAdmin/index.jsx
Normal file
@@ -0,0 +1,435 @@
|
|||||||
|
import { useState, useEffect, useRef } from "react";
|
||||||
|
import styles from "./Adminpage.module.scss";
|
||||||
|
|
||||||
|
// ─── PASSWORD — change this ───────────────────────────────────────────────────
|
||||||
|
const ADMIN_PASSWORD = "shumoff2024";
|
||||||
|
|
||||||
|
// ─── LOGIN ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function LoginScreen({ onLogin }) {
|
||||||
|
const [input, setInput] = useState("");
|
||||||
|
const [error, setError] = useState(false);
|
||||||
|
const [shake, setShake] = useState(false);
|
||||||
|
|
||||||
|
function handleSubmit(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (input === ADMIN_PASSWORD) {
|
||||||
|
onLogin();
|
||||||
|
} else {
|
||||||
|
setError(true);
|
||||||
|
setShake(true);
|
||||||
|
setTimeout(() => setShake(false), 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.loginWrapper}>
|
||||||
|
<div className={`${styles.loginCard} ${shake ? styles.shake : ""}`}>
|
||||||
|
<div className={styles.loginLogo}>⚙️</div>
|
||||||
|
<h1 className={styles.loginTitle}>Панель администратора</h1>
|
||||||
|
<p className={styles.loginSub}>Введите пароль для доступа</p>
|
||||||
|
<form onSubmit={handleSubmit} className={styles.loginForm}>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
className={`${styles.loginInput} ${error ? styles.inputError : ""}`}
|
||||||
|
placeholder="Пароль"
|
||||||
|
value={input}
|
||||||
|
onChange={(e) => { setInput(e.target.value); setError(false); }}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
{error && <p className={styles.errorMsg}>Неверный пароль. Попробуйте ещё раз.</p>}
|
||||||
|
<button type="submit" className={styles.loginBtn}>Войти</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── TABS ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const ADMIN_TABS = [
|
||||||
|
{ id: "products", label: "📦 Товары и цены" },
|
||||||
|
{ id: "bodytypes", label: "🚗 Типы кузова" },
|
||||||
|
];
|
||||||
|
|
||||||
|
// ─── ADMIN PANEL ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function AdminPanel({ onLogout }) {
|
||||||
|
const [data, setData] = useState(null);
|
||||||
|
const [tab, setTab] = useState("products");
|
||||||
|
const [zone, setZone] = useState(null);
|
||||||
|
const [bodyType, setBodyType] = useState(null);
|
||||||
|
const [pkg, setPkg] = useState(null);
|
||||||
|
const [saved, setSaved] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
|
// Load data.json from /public
|
||||||
|
useEffect(() => {
|
||||||
|
fetch("/frontend-api/data")
|
||||||
|
.then((r) => r.json())
|
||||||
|
.then((d) => {
|
||||||
|
setData(d);
|
||||||
|
setZone(Object.keys(d.zones)[0]);
|
||||||
|
setBodyType(d.bodyTypes[0]?.id);
|
||||||
|
setPkg(d.packages[0]);
|
||||||
|
setLoading(false);
|
||||||
|
})
|
||||||
|
.catch((e) => { setError(e.message); setLoading(false); });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// ── helpers ────────────────────────────────────────────────
|
||||||
|
function updateData(fn) {
|
||||||
|
setData((prev) => {
|
||||||
|
const next = JSON.parse(JSON.stringify(prev));
|
||||||
|
fn(next);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Products tab
|
||||||
|
function getProducts() {
|
||||||
|
return data?.zones?.[zone]?.products?.[bodyType]?.[pkg] || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function setProducts(products) {
|
||||||
|
updateData((d) => {
|
||||||
|
if (!d.zones[zone].products[bodyType]) d.zones[zone].products[bodyType] = {};
|
||||||
|
d.zones[zone].products[bodyType][pkg] = products;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateProduct(idx, field, val) {
|
||||||
|
const ps = JSON.parse(JSON.stringify(getProducts()));
|
||||||
|
if (field === "price" || field === "qty") ps[idx][field] = Number(val) || 0;
|
||||||
|
else ps[idx][field] = val;
|
||||||
|
setProducts(ps);
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteProduct(idx) {
|
||||||
|
const ps = JSON.parse(JSON.stringify(getProducts()));
|
||||||
|
ps.splice(idx, 1);
|
||||||
|
setProducts(ps);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addProduct() {
|
||||||
|
const ps = JSON.parse(JSON.stringify(getProducts()));
|
||||||
|
ps.push({ name: "Новый товар", price: 0, qty: 1, unit: "Л" });
|
||||||
|
setProducts(ps);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Body types tab
|
||||||
|
function updateBodyType(idx, field, val) {
|
||||||
|
updateData((d) => { d.bodyTypes[idx][field] = val; });
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteBodyType(idx) {
|
||||||
|
const bt = data.bodyTypes[idx];
|
||||||
|
updateData((d) => {
|
||||||
|
d.bodyTypes.splice(idx, 1);
|
||||||
|
// remove products for this body type in all zones
|
||||||
|
Object.values(d.zones).forEach((z) => { delete z.products[bt.id]; });
|
||||||
|
});
|
||||||
|
if (bodyType === bt.id) setBodyType(data.bodyTypes[0]?.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addBodyType() {
|
||||||
|
const newId = `body_${Date.now()}`;
|
||||||
|
updateData((d) => {
|
||||||
|
d.bodyTypes.push({ id: newId, label: "Новый тип", icon: "🚗" });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zone label
|
||||||
|
function updateZoneLabel(val) {
|
||||||
|
updateData((d) => { d.zones[zone].label = val; });
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleImageUpload(e, idx) {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (event) => {
|
||||||
|
updateBodyType(idx, "image", event.target.result);
|
||||||
|
updateBodyType(idx, "icon", null); // Eski icon verisini temizle
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Save = download updated data.json ──────────────────────
|
||||||
|
// Since there's no backend, admin downloads the JSON and replaces public/data.json
|
||||||
|
async function handleSave() {
|
||||||
|
const res = await fetch("/frontend-api/data", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
const result = await res.json();
|
||||||
|
if (!result.ok) { alert("Hata: " + result.error); return; }
|
||||||
|
setSaved(true);
|
||||||
|
setTimeout(() => setSaved(false), 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Render ─────────────────────────────────────────────────
|
||||||
|
if (loading) return <div className={styles.loadingScreen}>Загрузка...</div>;
|
||||||
|
if (error) return <div className={styles.loadingScreen}>Ошибка: {error}</div>;
|
||||||
|
|
||||||
|
const products = getProducts();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.adminWrapper}>
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
|
<header className={styles.adminHeader}>
|
||||||
|
<div className={styles.adminHeaderLeft}>
|
||||||
|
<span className={styles.adminHeaderIcon}>⚙️</span>
|
||||||
|
<div>
|
||||||
|
<h1 className={styles.adminHeaderTitle}>Панель администратора</h1>
|
||||||
|
<p className={styles.adminHeaderSub}>Управление товарами, ценами и типами кузова</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.adminHeaderRight}>
|
||||||
|
{saved && (
|
||||||
|
<span className={styles.savedBadge}>
|
||||||
|
✓ Сохранено
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<button className={styles.btnSaveHeader} onClick={handleSave}>
|
||||||
|
💾 Сохранить
|
||||||
|
</button>
|
||||||
|
<button className={styles.btnLogout} onClick={onLogout}>Выйти</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* Tab bar */}
|
||||||
|
<div className={styles.tabBar}>
|
||||||
|
{ADMIN_TABS.map((t) => (
|
||||||
|
<button
|
||||||
|
key={t.id}
|
||||||
|
className={`${styles.tabBtn} ${tab === t.id ? styles.tabActive : ""}`}
|
||||||
|
onClick={() => setTab(t.id)}
|
||||||
|
>
|
||||||
|
{t.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ── PRODUCTS TAB ─────────────────────────────────────── */}
|
||||||
|
{tab === "products" && (
|
||||||
|
<div className={styles.adminBody}>
|
||||||
|
|
||||||
|
{/* Sidebar: zones */}
|
||||||
|
<aside className={styles.sidebar}>
|
||||||
|
<p className={styles.sidebarLabel}>Зоны</p>
|
||||||
|
{Object.entries(data.zones).map(([zid, z]) => (
|
||||||
|
<button
|
||||||
|
key={zid}
|
||||||
|
className={`${styles.sidebarItem} ${zone === zid ? styles.sidebarActive : ""}`}
|
||||||
|
onClick={() => setZone(zid)}
|
||||||
|
>
|
||||||
|
{z.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
{/* Main content */}
|
||||||
|
<main className={styles.content}>
|
||||||
|
|
||||||
|
{/* Zone label */}
|
||||||
|
<div className={styles.fieldRow}>
|
||||||
|
<label className={styles.fieldLabel}>Название зоны</label>
|
||||||
|
<input
|
||||||
|
className={styles.fieldInput}
|
||||||
|
value={data.zones[zone]?.label || ""}
|
||||||
|
onChange={(e) => updateZoneLabel(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Body type selector */}
|
||||||
|
<div className={styles.bodyTypeRow}>
|
||||||
|
<span className={styles.fieldLabel}>Тип кузова</span>
|
||||||
|
<div className={styles.bodyTypePills}>
|
||||||
|
{data.bodyTypes.map((b) => (
|
||||||
|
<button
|
||||||
|
key={b.id}
|
||||||
|
className={`${styles.bodyPill} ${bodyType === b.id ? styles.bodyPillActive : ""}`}
|
||||||
|
onClick={() => setBodyType(b.id)}
|
||||||
|
>
|
||||||
|
{b.image ? <img src={b.image} alt={b.label} className={styles.bodyPillIcon} /> : (b.icon && <span className={styles.bodyPillEmoji}>{b.icon}</span>)}
|
||||||
|
{b.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Package tabs */}
|
||||||
|
<div className={styles.pkgTabs}>
|
||||||
|
{data.packages.map((p) => (
|
||||||
|
<button
|
||||||
|
key={p}
|
||||||
|
className={`${styles.pkgTab} ${pkg === p ? styles.pkgActive : ""}`}
|
||||||
|
onClick={() => setPkg(p)}
|
||||||
|
>
|
||||||
|
{p}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Products table */}
|
||||||
|
<div className={styles.tableWrapper}>
|
||||||
|
<table className={styles.table}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th className={styles.th}>Название товара</th>
|
||||||
|
<th className={`${styles.th} ${styles.thNum}`}>Цена (m)</th>
|
||||||
|
<th className={`${styles.th} ${styles.thNum}`}>Кол-во</th>
|
||||||
|
<th className={`${styles.th} ${styles.thNum}`}>Ед.</th>
|
||||||
|
<th className={`${styles.th} ${styles.thNum}`}>Итого</th>
|
||||||
|
<th className={styles.th}></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{products.map((p, i) => (
|
||||||
|
<tr key={i} className={styles.tr}>
|
||||||
|
<td className={styles.td}>
|
||||||
|
<input
|
||||||
|
className={styles.cellInput}
|
||||||
|
value={p.name}
|
||||||
|
onChange={(e) => updateProduct(i, "name", e.target.value)}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td className={styles.td}>
|
||||||
|
<input
|
||||||
|
className={`${styles.cellInput} ${styles.cellNum}`}
|
||||||
|
type="number" min="0"
|
||||||
|
value={p.price}
|
||||||
|
onChange={(e) => updateProduct(i, "price", e.target.value)}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td className={styles.td}>
|
||||||
|
<input
|
||||||
|
className={`${styles.cellInput} ${styles.cellNum}`}
|
||||||
|
type="number" min="0"
|
||||||
|
value={p.qty}
|
||||||
|
onChange={(e) => updateProduct(i, "qty", e.target.value)}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td className={styles.td}>
|
||||||
|
<input
|
||||||
|
className={`${styles.cellInput} ${styles.cellNum}`}
|
||||||
|
value={p.unit}
|
||||||
|
onChange={(e) => updateProduct(i, "unit", e.target.value)}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td className={`${styles.td} ${styles.tdTotal}`}>
|
||||||
|
{(p.price * p.qty).toLocaleString("ru")} m
|
||||||
|
</td>
|
||||||
|
<td className={styles.td}>
|
||||||
|
<button className={styles.btnDel} onClick={() => deleteProduct(i)}>✕</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
{products.length === 0 && (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={6} className={styles.emptyRow}>
|
||||||
|
Нет товаров для этой комбинации
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button className={styles.btnAdd} onClick={addProduct}>
|
||||||
|
+ Добавить товар
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ── BODY TYPES TAB ───────────────────────────────────── */}
|
||||||
|
{tab === "bodytypes" && (
|
||||||
|
<div className={styles.bodyTypesPage}>
|
||||||
|
<div className={styles.bodyTypesHeader}>
|
||||||
|
<h2 className={styles.bodyTypesTitle}>Типы кузова</h2>
|
||||||
|
<p className={styles.bodyTypesSub}>
|
||||||
|
Добавляйте, удаляйте и редактируйте типы кузова. Изменения применяются
|
||||||
|
ко всем зонам и пакетам.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.bodyTypesGrid}>
|
||||||
|
{data.bodyTypes.map((b, i) => (
|
||||||
|
<div key={b.id} className={styles.bodyTypeCard}>
|
||||||
|
<div className={styles.imageUploader}>
|
||||||
|
{b.image ? (
|
||||||
|
<img src={b.image} alt="Preview" className={styles.iconPreview} />
|
||||||
|
) : (
|
||||||
|
b.icon && <span className={styles.iconPreview}>{b.icon}</span>
|
||||||
|
)}
|
||||||
|
<label className={styles.uploadLabel}>
|
||||||
|
<span>{b.image || b.icon ? 'Изменить' : 'Загрузить'}</span>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="image/png, image/jpeg, image/svg+xml"
|
||||||
|
onChange={(e) => handleImageUpload(e, i)}
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className={styles.bodyTypeCardBottom}>
|
||||||
|
<input
|
||||||
|
className={styles.fieldInput}
|
||||||
|
value={b.label}
|
||||||
|
onChange={(e) => updateBodyType(i, "label", e.target.value)}
|
||||||
|
placeholder="Название типа"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className={styles.btnDel}
|
||||||
|
onClick={() => {
|
||||||
|
if (window.confirm(`Удалить тип "${b.label}"? Все товары для этого типа будут удалены.`)) {
|
||||||
|
deleteBodyType(i);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M18 6L6 18M6 6l12 12" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Add new */}
|
||||||
|
<button className={styles.bodyTypeAddCard} onClick={addBodyType}>
|
||||||
|
<span>+</span>
|
||||||
|
<span>Добавить тип</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.saveHint}>
|
||||||
|
<p>
|
||||||
|
После всех изменений нажмите <strong>«Сохранить»</strong> в шапке.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Floating save */}
|
||||||
|
<div className={styles.floatSave}>
|
||||||
|
<button className={styles.btnSaveFloat} onClick={handleSave}>
|
||||||
|
💾 Сохранить
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── PAGE EXPORT ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export default function AdminPage() {
|
||||||
|
const [authed, setAuthed] = useState(false);
|
||||||
|
return authed
|
||||||
|
? <AdminPanel onLogout={() => setAuthed(false)} />
|
||||||
|
: <LoginScreen onLogin={() => setAuthed(true)} />;
|
||||||
|
}
|
||||||
@@ -16,10 +16,9 @@
|
|||||||
.cartHeader {
|
.cartHeader {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 12px;
|
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
background-color: #f3f4f6;
|
background-color: #f3f4f6;
|
||||||
padding-bottom: 15px;
|
padding-top: 10px;
|
||||||
h2 {
|
h2 {
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
@@ -27,11 +26,11 @@
|
|||||||
@media screen and (max-width: 768px) {
|
@media screen and (max-width: 768px) {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
.cartProducts {
|
.cartProducts {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -152,6 +151,7 @@
|
|||||||
@media screen and (max-width: 720px) {
|
@media screen and (max-width: 720px) {
|
||||||
flex-direction: row-reverse;
|
flex-direction: row-reverse;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
gap:10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.price {
|
.price {
|
||||||
@@ -226,7 +226,7 @@
|
|||||||
@media screen and (max-width: 1023px) {
|
@media screen and (max-width: 1023px) {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
position: static;
|
position: static;
|
||||||
margin-top: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
@@ -524,3 +524,106 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pendingPriceBadgeWrapper {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pendingPriceBadge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #faeeda;
|
||||||
|
border: 0.5px solid #ef9f27;
|
||||||
|
color: #854f0b;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pendingPriceTooltip {
|
||||||
|
position: absolute;
|
||||||
|
bottom: calc(100% + 6px);
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background: var(--color-background-primary, #ffffff);
|
||||||
|
border: 0.5px solid var(--color-border-secondary, #e2e2e2);
|
||||||
|
border-radius: var(--border-radius-md, 6px);
|
||||||
|
padding: 8px 12px;
|
||||||
|
width: 220px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--color-text-primary, #333333);
|
||||||
|
line-height: 1.5;
|
||||||
|
z-index: 100;
|
||||||
|
white-space: normal;
|
||||||
|
pointer-events: none;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
strong {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
color: var(--color-text-primary, #000000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:global {
|
||||||
|
.pending-price-modal {
|
||||||
|
.ant-modal-content {
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-modal-header {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
.ant-modal-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-modal-body {
|
||||||
|
p {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #555;
|
||||||
|
margin: 0;
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-modal-footer {
|
||||||
|
margin-top: 20px;
|
||||||
|
.ant-btn-primary {
|
||||||
|
background-color: #888888;
|
||||||
|
border-color: #888888;
|
||||||
|
border-radius: 6px;
|
||||||
|
height: 36px;
|
||||||
|
padding: 0 20px;
|
||||||
|
font-weight: 500;
|
||||||
|
&:hover {
|
||||||
|
background-color: #666666;
|
||||||
|
border-color: #666666;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,13 +2,10 @@ import React, { useState, useRef, useEffect, useMemo } from "react";
|
|||||||
import styles from "./CartPage.module.scss";
|
import styles from "./CartPage.module.scss";
|
||||||
import { FaTrashAlt } from "react-icons/fa";
|
import { FaTrashAlt } from "react-icons/fa";
|
||||||
import Checkout from "../../components/Checkout";
|
import Checkout from "../../components/Checkout";
|
||||||
import { ChevronDown, ChevronUp } from "lucide-react";
|
|
||||||
import { Modal } from "antd";
|
import { Modal } from "antd";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import EmptyCartState from "./emptyCart";
|
import EmptyCartState from "./emptyCart";
|
||||||
import {
|
import {
|
||||||
useGetCartQuery,
|
|
||||||
useAddToCartMutation,
|
|
||||||
useRemoveFromCartMutation,
|
useRemoveFromCartMutation,
|
||||||
useUpdateCartItemMutation,
|
useUpdateCartItemMutation,
|
||||||
useCleanCartMutation,
|
useCleanCartMutation,
|
||||||
@@ -16,10 +13,11 @@ import {
|
|||||||
import { useCart } from "../../app/api/useCart";
|
import { useCart } from "../../app/api/useCart";
|
||||||
import { DecreaseIcon, IncreaseIcon } from "../../components/Icons";
|
import { DecreaseIcon, IncreaseIcon } from "../../components/Icons";
|
||||||
import Loader from "../../components/Loader/index";
|
import Loader from "../../components/Loader/index";
|
||||||
|
import PendingPriceBadge from "../../components/PendingPriceBadge";
|
||||||
|
|
||||||
|
const isPriceZero = (price) => !price || parseFloat(price) === 0;
|
||||||
|
|
||||||
const TruncatedDescription = ({ description, maxLength = 100 }) => {
|
const TruncatedDescription = ({ description, maxLength = 100 }) => {
|
||||||
const [isExpanded, setIsExpanded] = useState(false);
|
|
||||||
|
|
||||||
const stripHtml = (html) => {
|
const stripHtml = (html) => {
|
||||||
const doc = new DOMParser().parseFromString(html, "text/html");
|
const doc = new DOMParser().parseFromString(html, "text/html");
|
||||||
return doc.body.textContent || "";
|
return doc.body.textContent || "";
|
||||||
@@ -32,9 +30,7 @@ const TruncatedDescription = ({ description, maxLength = 100 }) => {
|
|||||||
<div className={styles.truncatedDescription}>
|
<div className={styles.truncatedDescription}>
|
||||||
<div
|
<div
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: isExpanded
|
__html: shouldTruncate
|
||||||
? description
|
|
||||||
: shouldTruncate
|
|
||||||
? description.substring(0, maxLength) + "..."
|
? description.substring(0, maxLength) + "..."
|
||||||
: description,
|
: description,
|
||||||
}}
|
}}
|
||||||
@@ -44,20 +40,16 @@ const TruncatedDescription = ({ description, maxLength = 100 }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const CartPage = () => {
|
const CartPage = () => {
|
||||||
const { cartData, cartItems, isLoading, isError, error } = useCart();
|
const { cartData, cartItems, isLoading } = useCart();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { t, i18n } = useTranslation();
|
|
||||||
const [checkoutStores, setCheckoutStores] = useState({});
|
const [checkoutStores, setCheckoutStores] = useState({});
|
||||||
const [addToCart] = useAddToCartMutation();
|
|
||||||
const [removeFromCart] = useRemoveFromCartMutation();
|
const [removeFromCart] = useRemoveFromCartMutation();
|
||||||
const [updateCartItem] = useUpdateCartItemMutation();
|
const [updateCartItem] = useUpdateCartItemMutation();
|
||||||
const [cleanCart] = useCleanCartMutation();
|
const [cleanCart] = useCleanCartMutation();
|
||||||
const [isExpanded, setIsExpanded] = useState(false);
|
|
||||||
const expandedRef = useRef(null);
|
|
||||||
const [deleteModalVisible, setDeleteModalVisible] = useState(false);
|
const [deleteModalVisible, setDeleteModalVisible] = useState(false);
|
||||||
const [emptyCartModalVisible, setEmptyCartModalVisible] = useState(false);
|
const [emptyCartModalVisible, setEmptyCartModalVisible] = useState(false);
|
||||||
const [itemToDelete, setItemToDelete] = useState(null);
|
const [itemToDelete, setItemToDelete] = useState(null);
|
||||||
|
|
||||||
const [localQuantities, setLocalQuantities] = useState({});
|
const [localQuantities, setLocalQuantities] = useState({});
|
||||||
const [pendingQuantities, setPendingQuantities] = useState({});
|
const [pendingQuantities, setPendingQuantities] = useState({});
|
||||||
const [loadingItems, setLoadingItems] = useState({});
|
const [loadingItems, setLoadingItems] = useState({});
|
||||||
@@ -70,43 +62,35 @@ const CartPage = () => {
|
|||||||
width: 400,
|
width: 400,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Convert grouped data to stores array
|
|
||||||
const stores = useMemo(() => {
|
const stores = useMemo(() => {
|
||||||
return Object.entries(cartData)
|
return Object.entries(cartData)
|
||||||
.map(([storeSlug, items]) => {
|
.map(([storeSlug, items]) => {
|
||||||
if (!items || !items.length) return null;
|
if (!items?.length) return null;
|
||||||
|
|
||||||
// Get store info from first item
|
|
||||||
const storeInfo = items[0]?.product?.channel?.[0];
|
const storeInfo = items[0]?.product?.channel?.[0];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: storeInfo?.id || storeSlug,
|
id: storeInfo?.id || storeSlug,
|
||||||
name: storeInfo?.name || storeSlug,
|
name: storeInfo?.name || storeSlug,
|
||||||
slug: storeSlug,
|
slug: storeSlug,
|
||||||
shipping_price: storeInfo?.shipping_price,
|
shipping_price: storeInfo?.shipping_price,
|
||||||
items: items,
|
items,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
}, [cartData]);
|
}, [cartData]);
|
||||||
|
|
||||||
// ✅ Initialize local quantities from cart items
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const newLocalQuantities = {};
|
const newLocal = {};
|
||||||
const newPendingQuantities = {};
|
const newPending = {};
|
||||||
|
|
||||||
cartItems.forEach((item) => {
|
cartItems.forEach((item) => {
|
||||||
const productId = item.product.id;
|
const id = item.product.id;
|
||||||
const quantity = parseInt(item.product_quantity, 10) || 0;
|
const qty = parseInt(item.product_quantity, 10) || 0;
|
||||||
newLocalQuantities[productId] = quantity;
|
newLocal[id] = qty;
|
||||||
newPendingQuantities[productId] = quantity;
|
newPending[id] = qty;
|
||||||
});
|
});
|
||||||
|
setLocalQuantities(newLocal);
|
||||||
setLocalQuantities(newLocalQuantities);
|
setPendingQuantities(newPending);
|
||||||
setPendingQuantities(newPendingQuantities);
|
|
||||||
}, [cartItems]);
|
}, [cartItems]);
|
||||||
|
|
||||||
// ✅ Debounced Cart Update - Her ürün için ayrı debounce
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timers = {};
|
const timers = {};
|
||||||
|
|
||||||
@@ -114,141 +98,94 @@ const CartPage = () => {
|
|||||||
const serverItem = cartItems.find(
|
const serverItem = cartItems.find(
|
||||||
(item) => String(item.product.id) === String(productId),
|
(item) => String(item.product.id) === String(productId),
|
||||||
);
|
);
|
||||||
const serverQuantity = serverItem
|
const serverQty = serverItem
|
||||||
? parseInt(serverItem.product_quantity, 10)
|
? parseInt(serverItem.product_quantity, 10)
|
||||||
: 0;
|
: 0;
|
||||||
const pendingQuantity = pendingQuantities[productId];
|
const pendingQty = pendingQuantities[productId];
|
||||||
|
|
||||||
// Değişiklik yoksa veya 0 ise (Delete modalı tetikler) bir şey yapma
|
|
||||||
if (
|
if (
|
||||||
pendingQuantity === undefined ||
|
pendingQty === undefined ||
|
||||||
pendingQuantity === serverQuantity ||
|
pendingQty === serverQty ||
|
||||||
pendingQuantity <= 0
|
pendingQty <= 0
|
||||||
) {
|
)
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
timers[productId] = setTimeout(async () => {
|
timers[productId] = setTimeout(async () => {
|
||||||
try {
|
try {
|
||||||
setLoadingItems((prev) => ({ ...prev, [productId]: true }));
|
setLoadingItems((prev) => ({ ...prev, [productId]: true }));
|
||||||
await updateCartItem({
|
await updateCartItem({ productId, quantity: pendingQty }).unwrap();
|
||||||
productId,
|
} catch {
|
||||||
quantity: pendingQuantity,
|
setLocalQuantities((prev) => ({ ...prev, [productId]: serverQty }));
|
||||||
}).unwrap();
|
setPendingQuantities((prev) => ({ ...prev, [productId]: serverQty }));
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to update cart:", error);
|
|
||||||
// Hata durumunda rollback
|
|
||||||
setLocalQuantities((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[productId]: serverQuantity,
|
|
||||||
}));
|
|
||||||
setPendingQuantities((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[productId]: serverQuantity,
|
|
||||||
}));
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingItems((prev) => ({ ...prev, [productId]: false }));
|
setLoadingItems((prev) => ({ ...prev, [productId]: false }));
|
||||||
}
|
}
|
||||||
}, 500);
|
}, 500);
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => Object.values(timers).forEach(clearTimeout);
|
||||||
Object.values(timers).forEach((timer) => clearTimeout(timer));
|
|
||||||
};
|
|
||||||
}, [pendingQuantities, cartItems, updateCartItem]);
|
}, [pendingQuantities, cartItems, updateCartItem]);
|
||||||
|
|
||||||
const handleQuantityIncrease = (productId) => (event) => {
|
const handleQuantityIncrease = (productId) => (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
if (loadingItems[productId]) return;
|
if (loadingItems[productId]) return;
|
||||||
|
|
||||||
const item = cartItems.find((item) => item.product.id === productId);
|
const item = cartItems.find((i) => i.product.id === productId);
|
||||||
if (!item) return;
|
if (!item || localQuantities[productId] >= item.product.stock) return;
|
||||||
|
|
||||||
if (localQuantities[productId] >= item.product.stock) {
|
const newQty = (localQuantities[productId] || 0) + 1;
|
||||||
return;
|
setLocalQuantities((prev) => ({ ...prev, [productId]: newQty }));
|
||||||
}
|
setPendingQuantities((prev) => ({ ...prev, [productId]: newQty }));
|
||||||
|
|
||||||
const newQuantity = (localQuantities[productId] || 0) + 1;
|
|
||||||
setLocalQuantities((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[productId]: newQuantity,
|
|
||||||
}));
|
|
||||||
setPendingQuantities((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[productId]: newQuantity,
|
|
||||||
}));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleQuantityDecrease = (productId) => (event) => {
|
const handleQuantityDecrease = (productId) => (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
if (loadingItems[productId]) return;
|
if (loadingItems[productId]) return;
|
||||||
|
|
||||||
const currentQuantity = localQuantities[productId] || 0;
|
const currentQty = localQuantities[productId] || 0;
|
||||||
|
if (currentQty <= 1) {
|
||||||
if (currentQuantity <= 1) {
|
|
||||||
showDeleteConfirm(productId);
|
showDeleteConfirm(productId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newQuantity = currentQuantity - 1;
|
const newQty = currentQty - 1;
|
||||||
setLocalQuantities((prev) => ({
|
setLocalQuantities((prev) => ({ ...prev, [productId]: newQty }));
|
||||||
...prev,
|
setPendingQuantities((prev) => ({ ...prev, [productId]: newQty }));
|
||||||
[productId]: newQuantity,
|
|
||||||
}));
|
|
||||||
setPendingQuantities((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[productId]: newQuantity,
|
|
||||||
}));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const calculateStoreTotal = (storeItems) => {
|
const getStoreShippingPrice = (store) =>
|
||||||
return storeItems.reduce((sum, item) => {
|
store.shipping_price != null ? parseFloat(store.shipping_price) : 20;
|
||||||
const itemPrice = parseFloat(item.product.price_amount) || 0;
|
|
||||||
const itemQuantity = parseInt(item.product_quantity, 10) || 0;
|
// Store içinde fiyatsız ürün var mı?
|
||||||
return sum + itemPrice * itemQuantity;
|
const storeHasZeroPriceItem = (storeItems) =>
|
||||||
|
storeItems.some((item) => isPriceZero(item.product.price_amount));
|
||||||
|
|
||||||
|
const calculateStoreTotal = (storeItems) =>
|
||||||
|
storeItems.reduce((sum, item) => {
|
||||||
|
return (
|
||||||
|
sum +
|
||||||
|
(parseFloat(item.product.price_amount) || 0) *
|
||||||
|
(parseInt(item.product_quantity, 10) || 0)
|
||||||
|
);
|
||||||
}, 0);
|
}, 0);
|
||||||
};
|
|
||||||
|
|
||||||
const getStoreShippingPrice = (store) => {
|
const handleCheckout = (storeId) =>
|
||||||
return store.shipping_price !== null && store.shipping_price !== undefined
|
|
||||||
? parseFloat(store.shipping_price)
|
|
||||||
: 20;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCheckout = (storeId) => {
|
|
||||||
setCheckoutStores((prev) => ({ ...prev, [storeId]: true }));
|
setCheckoutStores((prev) => ({ ...prev, [storeId]: true }));
|
||||||
};
|
|
||||||
|
|
||||||
const handleBackToCart = (storeId) => {
|
const handleBackToCart = (storeId) =>
|
||||||
setCheckoutStores((prev) => ({ ...prev, [storeId]: false }));
|
setCheckoutStores((prev) => ({ ...prev, [storeId]: false }));
|
||||||
};
|
|
||||||
|
|
||||||
const handleOrderSubmit = async (storeId, storeItems) => {
|
const handleOrderSubmit = async (storeId) => {
|
||||||
if (checkoutStores[storeId] && checkoutRefs.current[storeId]) {
|
if (checkoutStores[storeId] && checkoutRefs.current[storeId]) {
|
||||||
const success = await checkoutRefs.current[storeId]();
|
const success = await checkoutRefs.current[storeId]();
|
||||||
if (success) {
|
if (success) setCheckoutStores((prev) => ({ ...prev, [storeId]: false }));
|
||||||
setCheckoutStores((prev) => ({ ...prev, [storeId]: false }));
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
handleCheckout(storeId);
|
handleCheckout(storeId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleClickOutside = (event) => {
|
|
||||||
if (expandedRef.current && !expandedRef.current.contains(event.target)) {
|
|
||||||
setIsExpanded(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener("mousedown", handleClickOutside);
|
|
||||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const showDeleteConfirm = (productId) => {
|
const showDeleteConfirm = (productId) => {
|
||||||
setItemToDelete(productId);
|
setItemToDelete(productId);
|
||||||
setDeleteModalVisible(true);
|
setDeleteModalVisible(true);
|
||||||
@@ -258,48 +195,41 @@ const CartPage = () => {
|
|||||||
if (itemToDelete) {
|
if (itemToDelete) {
|
||||||
try {
|
try {
|
||||||
await removeFromCart({ productId: itemToDelete }).unwrap();
|
await removeFromCart({ productId: itemToDelete }).unwrap();
|
||||||
|
|
||||||
setLocalQuantities((prev) => {
|
setLocalQuantities((prev) => {
|
||||||
const newState = { ...prev };
|
const s = { ...prev };
|
||||||
delete newState[itemToDelete];
|
delete s[itemToDelete];
|
||||||
return newState;
|
return s;
|
||||||
});
|
});
|
||||||
setPendingQuantities((prev) => {
|
setPendingQuantities((prev) => {
|
||||||
const newState = { ...prev };
|
const s = { ...prev };
|
||||||
delete newState[itemToDelete];
|
delete s[itemToDelete];
|
||||||
return newState;
|
return s;
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (e) {
|
||||||
console.error("Failed to remove item:", error);
|
console.error("Failed to remove item:", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setDeleteModalVisible(false);
|
setDeleteModalVisible(false);
|
||||||
setItemToDelete(null);
|
setItemToDelete(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const showEmptyCartConfirm = () => {
|
|
||||||
setEmptyCartModalVisible(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEmptyCartConfirm = async () => {
|
const handleEmptyCartConfirm = async () => {
|
||||||
try {
|
try {
|
||||||
await cleanCart().unwrap();
|
await cleanCart().unwrap();
|
||||||
|
|
||||||
setLocalQuantities({});
|
setLocalQuantities({});
|
||||||
setPendingQuantities({});
|
setPendingQuantities({});
|
||||||
setCheckoutStores({});
|
setCheckoutStores({});
|
||||||
} catch (error) {
|
} catch (e) {
|
||||||
console.error("Failed to clean cart:", error);
|
console.error("Failed to clean cart:", e);
|
||||||
}
|
}
|
||||||
setEmptyCartModalVisible(false);
|
setEmptyCartModalVisible(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getTotalItemCount = () => {
|
const getTotalItemCount = () =>
|
||||||
return cartItems.reduce(
|
cartItems.reduce(
|
||||||
(sum, item) => sum + parseInt(item.product_quantity, 10),
|
(sum, item) => sum + parseInt(item.product_quantity, 10),
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.cartContainer}>
|
<div className={styles.cartContainer}>
|
||||||
@@ -339,21 +269,20 @@ const CartPage = () => {
|
|||||||
<h2>
|
<h2>
|
||||||
{t("cart.basket")} ({getTotalItemCount()})
|
{t("cart.basket")} ({getTotalItemCount()})
|
||||||
</h2>
|
</h2>
|
||||||
<div>
|
|
||||||
<button
|
<button
|
||||||
className={styles.deleteBtn}
|
className={styles.deleteBtn}
|
||||||
style={{ padding: "4px 12px" }}
|
style={{ padding: "4px 12px" }}
|
||||||
onClick={showEmptyCartConfirm}
|
onClick={() => setEmptyCartModalVisible(true)}
|
||||||
>
|
>
|
||||||
<FaTrashAlt /> {t("cart.clearCart")}
|
<FaTrashAlt /> {t("cart.clearCart")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{stores.map((store) => {
|
{stores.map((store) => {
|
||||||
const shippingPrice = getStoreShippingPrice(store);
|
const shippingPrice = getStoreShippingPrice(store);
|
||||||
const storeTotal = calculateStoreTotal(store.items);
|
const storeTotal = calculateStoreTotal(store.items);
|
||||||
const totalWithShipping = storeTotal + shippingPrice;
|
const totalWithShipping = storeTotal + shippingPrice;
|
||||||
|
const hasZeroPrice = storeHasZeroPriceItem(store.items);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={store.id} className={styles.storeSection}>
|
<div key={store.id} className={styles.storeSection}>
|
||||||
@@ -363,8 +292,8 @@ const CartPage = () => {
|
|||||||
shippingPrice={shippingPrice}
|
shippingPrice={shippingPrice}
|
||||||
productIds={store.items.map((item) => item.product.id)}
|
productIds={store.items.map((item) => item.product.id)}
|
||||||
onBackToCart={() => handleBackToCart(store.id)}
|
onBackToCart={() => handleBackToCart(store.id)}
|
||||||
onPlaceOrder={(placeOrderFn) => {
|
onPlaceOrder={(fn) => {
|
||||||
checkoutRefs.current[store.id] = placeOrderFn;
|
checkoutRefs.current[store.id] = fn;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
@@ -391,10 +320,9 @@ const CartPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className={styles.priceQuantity}>
|
<div className={styles.priceQuantity}>
|
||||||
<span className={styles.price}>
|
<span className={styles.price}>
|
||||||
{(
|
{isPriceZero(item.product.price_amount)
|
||||||
parseFloat(item.product.price_amount) || 0
|
? t("cart.pendingPriceTitle")
|
||||||
).toFixed(2)}{" "}
|
: `${parseFloat(item.product.price_amount).toFixed(2)} m.`}
|
||||||
m.
|
|
||||||
</span>
|
</span>
|
||||||
<div className={styles.quantityControls}>
|
<div className={styles.quantityControls}>
|
||||||
<button
|
<button
|
||||||
@@ -441,11 +369,21 @@ const CartPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* ✅ Store Summary - fiyatsız ürün varsa "Baha anyklamak" */}
|
||||||
<div className={styles.storeSummary}>
|
<div className={styles.storeSummary}>
|
||||||
<div className={styles.cartContent}>
|
<div className={styles.cartContent}>
|
||||||
<h3>
|
<h3>
|
||||||
{store.name} - {t("cart.basket")}:
|
{store.name} - {t("cart.basket")}:
|
||||||
</h3>
|
</h3>
|
||||||
|
{hasZeroPrice ? (
|
||||||
|
<div className={styles.summaryRow}>
|
||||||
|
<span>{t("cart.total")}:</span>
|
||||||
|
<span style={{ display: "inline-flex", alignItems: "center", gap: 6 }}>
|
||||||
|
{t("cart.pendingPriceTitle")} <PendingPriceBadge />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
<div className={styles.summaryRow}>
|
<div className={styles.summaryRow}>
|
||||||
<span>{t("cart.price")}:</span>
|
<span>{t("cart.price")}:</span>
|
||||||
<span>{storeTotal.toFixed(2)} m.</span>
|
<span>{storeTotal.toFixed(2)} m.</span>
|
||||||
@@ -458,9 +396,11 @@ const CartPage = () => {
|
|||||||
<span>{t("cart.total")}:</span>
|
<span>{t("cart.total")}:</span>
|
||||||
<span>{totalWithShipping.toFixed(2)} m.</span>
|
<span>{totalWithShipping.toFixed(2)} m.</span>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleOrderSubmit(store.id, store.items)}
|
onClick={() => handleOrderSubmit(store.id)}
|
||||||
className={styles.checkoutBtn}
|
className={styles.checkoutBtn}
|
||||||
>
|
>
|
||||||
{checkoutStores[store.id]
|
{checkoutStores[store.id]
|
||||||
@@ -472,7 +412,6 @@ const CartPage = () => {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Mobile sticky summary */}
|
{/* Mobile sticky summary */}
|
||||||
{/* <div className={styles.container}>
|
{/* <div className={styles.container}>
|
||||||
<div className={styles.summaryCard} ref={expandedRef}>
|
<div className={styles.summaryCard} ref={expandedRef}>
|
||||||
|
|||||||
@@ -1,3 +1,96 @@
|
|||||||
|
.sortingSection {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.sortingTitle {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sortingButtonsContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
|
||||||
|
.sortingBtn {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-radius: 5px;
|
||||||
|
background: #fff;
|
||||||
|
color: #333;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #d32824;
|
||||||
|
background-color: #fff5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.activeSorting {
|
||||||
|
background-color: #d32824;
|
||||||
|
color: #fff;
|
||||||
|
border-color: #d32824;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sortingContainer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-left: 16px;
|
||||||
|
|
||||||
|
.sortingLabel {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.pricePresetsContainer {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
.pricePresetBtn {
|
||||||
|
padding: 7px 10px;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-radius: 5px;
|
||||||
|
background: #fff;
|
||||||
|
color: #333;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #d32824;
|
||||||
|
background-color: #fff5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.activePreset {
|
||||||
|
background-color: #d32824;
|
||||||
|
color: #fff;
|
||||||
|
border-color: #d32824;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.mobilePhoneGrid {
|
.mobilePhoneGrid {
|
||||||
display: flex !important;
|
display: flex !important;
|
||||||
@@ -7,41 +100,65 @@
|
|||||||
// Price Filter Styles
|
// Price Filter Styles
|
||||||
.priceFilterContainer {
|
.priceFilterContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 12px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
padding: 12px;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
|
||||||
|
animation: slideDown 0.2s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes slideDown {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-8px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.priceInputGroup {
|
.priceInputGroup {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 4px;
|
gap: 6px;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.priceLabel {
|
.priceLabel {
|
||||||
font-size: 13px;
|
font-size: 12px;
|
||||||
color: #888;
|
color: #666;
|
||||||
margin-bottom: 2px;
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.priceInput {
|
.priceInput {
|
||||||
width: 90px;
|
padding: 8px 10px;
|
||||||
padding: 6px 10px;
|
|
||||||
border: 1px solid #d1d5db;
|
border: 1px solid #d1d5db;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
font-size: 15px;
|
font-size: 14px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
transition: border-color 0.2s;
|
transition: all 0.2s ease;
|
||||||
|
width: 85%;
|
||||||
|
&::placeholder {
|
||||||
|
color: #bbb;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.priceInput:focus {
|
.priceInput:focus {
|
||||||
border-color: #6c63ff;
|
border-color: #d32824;
|
||||||
|
box-shadow: 0 0 0 3px rgba(211, 40, 36, 0.1);
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.priceDivider {
|
.priceDivider {
|
||||||
font-size: 18px;
|
display: none;
|
||||||
color: #aaa;
|
|
||||||
font-weight: bold;
|
|
||||||
margin: 0 6px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.filtersContainer{
|
.filtersContainer{
|
||||||
@@ -67,6 +184,16 @@
|
|||||||
color: #000000;
|
color: #000000;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #f3f4f6;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
transform: translateX(4px);
|
||||||
|
color: #d32824;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
label {
|
label {
|
||||||
@@ -282,10 +409,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.productGrid::-webkit-scrollbar {
|
||||||
|
display: none !important;
|
||||||
|
width: 0 !important;
|
||||||
|
height: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
.productsContainer {
|
.productsContainer {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
||||||
.productGrid {
|
.productGrid {
|
||||||
|
-ms-overflow-style: none !important;
|
||||||
|
scrollbar-width: none !important;
|
||||||
|
overflow: hidden !important;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(238px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(238px, 1fr));
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
@@ -364,3 +499,45 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.channelHeader {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
background: #fff;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
.channelLogo {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
border-radius: 50%;
|
||||||
|
object-fit: cover;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channelInfo {
|
||||||
|
h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
padding: 15px;
|
||||||
|
gap: 15px;
|
||||||
|
|
||||||
|
.channelLogo {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channelInfo h1 {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React from "react";
|
import React, { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { TiTick } from "react-icons/ti";
|
import { TiTick } from "react-icons/ti";
|
||||||
|
import { Divider } from "antd";
|
||||||
import styles from "../CategoryPage.module.scss";
|
import styles from "../CategoryPage.module.scss";
|
||||||
|
|
||||||
const CategoryFilters = ({
|
const CategoryFilters = ({
|
||||||
@@ -18,10 +19,37 @@ const CategoryFilters = ({
|
|||||||
onBrandSelect,
|
onBrandSelect,
|
||||||
onBrandDeselect,
|
onBrandDeselect,
|
||||||
onBrandSearchChange,
|
onBrandSearchChange,
|
||||||
|
sorting = "",
|
||||||
|
onSortingChange = () => {},
|
||||||
className,
|
className,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const pricePresets = [
|
||||||
|
{ label: t("category.under50"), min: 0, max: 50 },
|
||||||
|
{ label: t("category.under100"), min: 0, max: 100 },
|
||||||
|
{ label: t("category.from50to200"), min: 50, max: 200 },
|
||||||
|
{ label: t("category.from200to500"), min: 200, max: 500 },
|
||||||
|
{ label: t("category.from500to1000"), min: 500, max: 1000 },
|
||||||
|
{ label: t("category.over1000"), min: 1000, max: 999999 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const handlePricePreset = (preset) => {
|
||||||
|
// Eğer zaten aktifse, sıfırla
|
||||||
|
if (minPrice === preset.min.toString() && maxPrice === preset.max.toString()) {
|
||||||
|
onMinPriceChange("");
|
||||||
|
onMaxPriceChange("");
|
||||||
|
} else {
|
||||||
|
onMinPriceChange(preset.min.toString());
|
||||||
|
onMaxPriceChange(preset.max.toString());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const sortOptions = [
|
||||||
|
{ value: "price_amount-ascending", label: t("category.priceLowToHigh") },
|
||||||
|
{ value: "price_amount-descending", label: t("category.priceHighToLow") },
|
||||||
|
];
|
||||||
|
|
||||||
if (searchQuery) return null;
|
if (searchQuery) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -98,6 +126,23 @@ const CategoryFilters = ({
|
|||||||
)}
|
)}
|
||||||
<div className={styles.filterSection}>
|
<div className={styles.filterSection}>
|
||||||
<h3>{t("category.price")}</h3>
|
<h3>{t("category.price")}</h3>
|
||||||
|
|
||||||
|
<div className={styles.pricePresetsContainer}>
|
||||||
|
{pricePresets.map((preset, idx) => (
|
||||||
|
<button
|
||||||
|
key={idx}
|
||||||
|
className={`${styles.pricePresetBtn} ${
|
||||||
|
minPrice === preset.min.toString() && maxPrice === preset.max.toString()
|
||||||
|
? styles.activePreset
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
|
onClick={() => handlePricePreset(preset)}
|
||||||
|
>
|
||||||
|
{preset.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className={styles.priceFilterContainer}>
|
<div className={styles.priceFilterContainer}>
|
||||||
<div className={styles.priceInputGroup}>
|
<div className={styles.priceInputGroup}>
|
||||||
<span className={styles.priceLabel}>{t("category.minPrice")}</span>
|
<span className={styles.priceLabel}>{t("category.minPrice")}</span>
|
||||||
@@ -123,6 +168,27 @@ const CategoryFilters = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Divider style={{ margin: "12px 0" }} />
|
||||||
|
|
||||||
|
<div className={styles.sortingSection}>
|
||||||
|
<h4 className={styles.sortingTitle}>{t("category.sortBy")}</h4>
|
||||||
|
<div className={styles.sortingButtonsContainer}>
|
||||||
|
{sortOptions.map((option) => (
|
||||||
|
<button
|
||||||
|
key={option.value}
|
||||||
|
className={`${styles.sortingBtn} ${sorting === option.value ? styles.activeSorting : ""}`}
|
||||||
|
onClick={() => onSortingChange(option.value)}
|
||||||
|
aria-pressed={sorting === option.value}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
{sorting === option.value && (
|
||||||
|
<span style={{ marginLeft: 4, fontWeight: "bold" }}></span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,11 +5,13 @@ import {
|
|||||||
useGetFiltersQuery,
|
useGetFiltersQuery,
|
||||||
useLazyGetFiltersQuery,
|
useLazyGetFiltersQuery,
|
||||||
} from "../../../app/api/filtersApi";
|
} from "../../../app/api/filtersApi";
|
||||||
|
import { useGetChannelsQuery } from "../../../app/api/channelsApi";
|
||||||
|
|
||||||
const useCategoryData = ({
|
const useCategoryData = ({
|
||||||
categoryId,
|
categoryId,
|
||||||
collectionId,
|
collectionId,
|
||||||
brandId,
|
brandId,
|
||||||
|
channelId,
|
||||||
selectedFilterCategory,
|
selectedFilterCategory,
|
||||||
searchQuery,
|
searchQuery,
|
||||||
}) => {
|
}) => {
|
||||||
@@ -23,8 +25,9 @@ const useCategoryData = ({
|
|||||||
if (categoryId) return { category_id: categoryId };
|
if (categoryId) return { category_id: categoryId };
|
||||||
if (collectionId) return { collection_id: collectionId };
|
if (collectionId) return { collection_id: collectionId };
|
||||||
if (brandId) return { brand_id: brandId };
|
if (brandId) return { brand_id: brandId };
|
||||||
|
if (channelId) return { channel_id: channelId };
|
||||||
return null;
|
return null;
|
||||||
}, [categoryId, collectionId, brandId, selectedFilterCategory, searchQuery]);
|
}, [categoryId, collectionId, brandId, channelId, selectedFilterCategory, searchQuery]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: filtersData,
|
data: filtersData,
|
||||||
@@ -44,6 +47,19 @@ const useCategoryData = ({
|
|||||||
skip: !collectionId,
|
skip: !collectionId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: channelsListData,
|
||||||
|
isLoading: channelsLoading,
|
||||||
|
error: channelsError,
|
||||||
|
} = useGetChannelsQuery({ perPage: 100 }, {
|
||||||
|
skip: !channelId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const channelData = useMemo(() => {
|
||||||
|
if (!channelId || !channelsListData?.data) return null;
|
||||||
|
return channelsListData.data.find(c => String(c.id) === String(channelId));
|
||||||
|
}, [channelId, channelsListData]);
|
||||||
|
|
||||||
const isSubCategory = useMemo(() => {
|
const isSubCategory = useMemo(() => {
|
||||||
if (!categoriesData?.data || !categoryId) return false;
|
if (!categoriesData?.data || !categoryId) return false;
|
||||||
|
|
||||||
@@ -92,8 +108,8 @@ const useCategoryData = ({
|
|||||||
setSelectedCategory(category);
|
setSelectedCategory(category);
|
||||||
}, [categoryId, categoriesData]);
|
}, [categoryId, categoriesData]);
|
||||||
|
|
||||||
const isLoading = filtersLoading || collectionLoading;
|
const isLoading = filtersLoading || collectionLoading || channelsLoading;
|
||||||
const hasError = filtersError || collectionError;
|
const hasError = filtersError || collectionError || channelsError;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
categoriesData,
|
categoriesData,
|
||||||
@@ -101,6 +117,7 @@ const useCategoryData = ({
|
|||||||
isSubCategory,
|
isSubCategory,
|
||||||
filtersData: activeFilters,
|
filtersData: activeFilters,
|
||||||
collectionData,
|
collectionData,
|
||||||
|
channelData,
|
||||||
isLoading,
|
isLoading,
|
||||||
hasError,
|
hasError,
|
||||||
fetchFilters,
|
fetchFilters,
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
import { useState, useEffect, useMemo, useRef } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import {
|
import {
|
||||||
useGetCategoryProductsQuery,
|
|
||||||
useLazyGetAllCategoryProductsPaginatedQuery,
|
useLazyGetAllCategoryProductsPaginatedQuery,
|
||||||
|
useGetCategoryProductsQuery,
|
||||||
} from "../../../app/api/categories";
|
} from "../../../app/api/categories";
|
||||||
import { useLazyGetBrandProductsQuery } from "../../../app/api/brandsApi";
|
import { useLazyGetBrandProductsQuery } from "../../../app/api/brandsApi";
|
||||||
import { useLazyGetCollectionProductsPaginatedQuery } from "../../../app/api/collectionsApi";
|
import { useLazyGetCollectionProductsPaginatedQuery } from "../../../app/api/collectionsApi";
|
||||||
|
import { useLazyGetChannelProductsQuery } from "../../../app/api/channelsApi"; // EKLE
|
||||||
|
|
||||||
const useCategoryProducts = ({
|
const useCategoryProducts = ({
|
||||||
categoryId,
|
categoryId,
|
||||||
collectionId,
|
collectionId,
|
||||||
brandId,
|
brandId,
|
||||||
|
channelId,
|
||||||
selectedCategory,
|
selectedCategory,
|
||||||
isSubCategory,
|
isSubCategory,
|
||||||
currentPage,
|
currentPage,
|
||||||
@@ -17,296 +19,198 @@ const useCategoryProducts = ({
|
|||||||
selectedFilterBrand,
|
selectedFilterBrand,
|
||||||
minPrice,
|
minPrice,
|
||||||
maxPrice,
|
maxPrice,
|
||||||
|
sorting,
|
||||||
searchQuery,
|
searchQuery,
|
||||||
|
initialProducts = [],
|
||||||
|
initialHasMore = true,
|
||||||
}) => {
|
}) => {
|
||||||
const [products, setProducts] = useState([]);
|
const [products, setProducts] = useState(initialProducts);
|
||||||
const [hasMore, setHasMore] = useState(true);
|
const [hasMore, setHasMore] = useState(initialHasMore);
|
||||||
|
const [isFetching, setIsFetching] = useState(false);
|
||||||
|
|
||||||
const isFetchingRef = useRef(false);
|
const activeRequestId = useRef(0);
|
||||||
const lastFetchKeyRef = useRef(null);
|
|
||||||
const abortControllerRef = useRef(null);
|
|
||||||
|
|
||||||
const contextId = useMemo(() => {
|
|
||||||
const parts = [
|
|
||||||
selectedFilterCategory && `fcat-${selectedFilterCategory}`,
|
|
||||||
categoryId && `cat-${categoryId}`,
|
|
||||||
brandId && `brand-${brandId}`,
|
|
||||||
collectionId && `col-${collectionId}`,
|
|
||||||
selectedFilterBrand && `fbrand-${selectedFilterBrand}`,
|
|
||||||
minPrice && `min-${minPrice}`,
|
|
||||||
maxPrice && `max-${maxPrice}`,
|
|
||||||
].filter(Boolean);
|
|
||||||
return parts.join("|") || "none";
|
|
||||||
}, [
|
|
||||||
selectedFilterCategory,
|
|
||||||
categoryId,
|
|
||||||
brandId,
|
|
||||||
collectionId,
|
|
||||||
selectedFilterBrand,
|
|
||||||
minPrice,
|
|
||||||
maxPrice,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const fetchParams = useMemo(
|
|
||||||
() => ({
|
|
||||||
page: currentPage,
|
|
||||||
limit: 6,
|
|
||||||
brands: selectedFilterBrand || undefined,
|
|
||||||
min_price: minPrice || undefined,
|
|
||||||
max_price: maxPrice || undefined,
|
|
||||||
}),
|
|
||||||
[currentPage, selectedFilterBrand, minPrice, maxPrice],
|
|
||||||
);
|
|
||||||
|
|
||||||
const fetchKey = `${contextId}-p${currentPage}`;
|
|
||||||
|
|
||||||
const shouldUseBaseQuery =
|
const shouldUseBaseQuery =
|
||||||
categoryId &&
|
categoryId &&
|
||||||
!isSubCategory &&
|
!isSubCategory &&
|
||||||
!searchQuery &&
|
!searchQuery &&
|
||||||
!selectedFilterCategory &&
|
!selectedFilterCategory &&
|
||||||
!selectedFilterBrand &&
|
|
||||||
!brandId &&
|
!brandId &&
|
||||||
!collectionId;
|
!collectionId &&
|
||||||
|
!channelId;
|
||||||
|
|
||||||
const {
|
const { data: baseQueryData, isFetching: baseQueryFetching } =
|
||||||
data: paginatedCategoryProducts,
|
useGetCategoryProductsQuery(
|
||||||
isLoading: categoryLoading,
|
|
||||||
isFetching: categoryFetching,
|
|
||||||
} = useGetCategoryProductsQuery(
|
|
||||||
{
|
{
|
||||||
categoryId: categoryId,
|
categoryId,
|
||||||
page: currentPage,
|
page: currentPage,
|
||||||
min_price: minPrice || undefined,
|
min_price: minPrice || undefined,
|
||||||
max_price: maxPrice || undefined,
|
max_price: maxPrice || undefined,
|
||||||
|
brands: selectedFilterBrand || undefined,
|
||||||
|
sorting: sorting || undefined,
|
||||||
},
|
},
|
||||||
{
|
{ skip: !shouldUseBaseQuery }
|
||||||
skip: !shouldUseBaseQuery,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const [
|
const [fetchCategoryPaginated] = useLazyGetAllCategoryProductsPaginatedQuery();
|
||||||
|
const [fetchBrandPaginated] = useLazyGetBrandProductsQuery();
|
||||||
|
const [fetchCollectionPaginated] = useLazyGetCollectionProductsPaginatedQuery();
|
||||||
|
const [fetchChannelPaginated] = useLazyGetChannelProductsQuery();
|
||||||
|
|
||||||
|
// ✅ Ref'e al — dependency array'den çıkar, stale closure yok
|
||||||
|
const fetchersRef = useRef({});
|
||||||
|
fetchersRef.current = {
|
||||||
fetchCategoryPaginated,
|
fetchCategoryPaginated,
|
||||||
{
|
|
||||||
data: lazyCategoryProducts,
|
|
||||||
isLoading: lazyCategoryLoading,
|
|
||||||
isFetching: lazyCategoryFetching,
|
|
||||||
reset: resetCategoryPaginated,
|
|
||||||
},
|
|
||||||
] = useLazyGetAllCategoryProductsPaginatedQuery();
|
|
||||||
|
|
||||||
const [
|
|
||||||
fetchBrandPaginated,
|
fetchBrandPaginated,
|
||||||
{
|
|
||||||
data: paginatedBrandProducts,
|
|
||||||
isLoading: brandPaginatedLoading,
|
|
||||||
isFetching: brandFetching,
|
|
||||||
reset: resetBrandPaginated,
|
|
||||||
},
|
|
||||||
] = useLazyGetBrandProductsQuery();
|
|
||||||
|
|
||||||
const [
|
|
||||||
fetchCollectionPaginated,
|
fetchCollectionPaginated,
|
||||||
{
|
fetchChannelPaginated,
|
||||||
data: paginatedCollectionProducts,
|
};
|
||||||
isLoading: collectionPaginatedLoading,
|
|
||||||
isFetching: collectionFetching,
|
|
||||||
reset: resetCollectionPaginated,
|
|
||||||
},
|
|
||||||
] = useLazyGetCollectionProductsPaginatedQuery();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setProducts([]);
|
if (!shouldUseBaseQuery || !baseQueryData) return;
|
||||||
setHasMore(true);
|
const data = baseQueryData.data || [];
|
||||||
|
const hasNextPage = !!baseQueryData.pagination?.next_page_url;
|
||||||
|
setProducts((prev) => {
|
||||||
|
if (currentPage === 1) return data;
|
||||||
|
const existingIds = new Set(prev.map((p) => p.id));
|
||||||
|
const newItems = data.filter((p) => !existingIds.has(p.id));
|
||||||
|
return newItems.length > 0 ? [...prev, ...newItems] : prev;
|
||||||
|
});
|
||||||
|
setHasMore(hasNextPage);
|
||||||
|
}, [baseQueryData, currentPage, shouldUseBaseQuery]);
|
||||||
|
|
||||||
resetCategoryPaginated?.();
|
|
||||||
resetBrandPaginated?.();
|
|
||||||
resetCollectionPaginated?.();
|
|
||||||
|
|
||||||
lastFetchKeyRef.current = null;
|
|
||||||
isFetchingRef.current = false;
|
|
||||||
|
|
||||||
if (abortControllerRef.current) {
|
|
||||||
abortControllerRef.current.abort();
|
|
||||||
abortControllerRef.current = null;
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
contextId,
|
|
||||||
resetCategoryPaginated,
|
|
||||||
resetBrandPaginated,
|
|
||||||
resetCollectionPaginated,
|
|
||||||
]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (searchQuery) return;
|
if (shouldUseBaseQuery || searchQuery) return;
|
||||||
|
|
||||||
if (lastFetchKeyRef.current === fetchKey) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isFetchingRef.current) {
|
console.log("🔥 LAZY EFFECT TRIGGERED", {
|
||||||
return;
|
shouldUseBaseQuery,
|
||||||
}
|
categoryId,
|
||||||
|
|
||||||
if (abortControllerRef.current) {
|
|
||||||
abortControllerRef.current.abort();
|
|
||||||
}
|
|
||||||
|
|
||||||
abortControllerRef.current = new AbortController();
|
|
||||||
isFetchingRef.current = true;
|
|
||||||
lastFetchKeyRef.current = fetchKey;
|
|
||||||
|
|
||||||
const executeFetch = async () => {
|
|
||||||
try {
|
|
||||||
if (selectedFilterBrand) {
|
|
||||||
await fetchBrandPaginated({
|
|
||||||
id: selectedFilterBrand,
|
|
||||||
...fetchParams,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedFilterCategory) {
|
|
||||||
await fetchCategoryPaginated({
|
|
||||||
category: {
|
|
||||||
id: selectedFilterCategory,
|
|
||||||
children: [],
|
|
||||||
},
|
|
||||||
...fetchParams,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (categoryId && isSubCategory) {
|
|
||||||
await fetchCategoryPaginated({
|
|
||||||
category: {
|
|
||||||
id: parseInt(categoryId),
|
|
||||||
children: [],
|
|
||||||
},
|
|
||||||
...fetchParams,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (brandId) {
|
|
||||||
await fetchBrandPaginated({
|
|
||||||
id: brandId,
|
|
||||||
...fetchParams,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (collectionId) {
|
|
||||||
await fetchCollectionPaginated({
|
|
||||||
collectionId,
|
collectionId,
|
||||||
...fetchParams,
|
brandId,
|
||||||
|
channelId,
|
||||||
|
isSubCategory,
|
||||||
|
selectedFilterCategory,
|
||||||
|
selectedCategory,
|
||||||
});
|
});
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (error.name !== "AbortError") {
|
|
||||||
console.error("Fetch error:", error);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
isFetchingRef.current = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
executeFetch();
|
const snapshot = {
|
||||||
|
currentPage,
|
||||||
return () => {
|
|
||||||
if (abortControllerRef.current) {
|
|
||||||
abortControllerRef.current.abort();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [
|
|
||||||
fetchKey,
|
|
||||||
searchQuery,
|
|
||||||
selectedFilterBrand,
|
|
||||||
selectedFilterCategory,
|
selectedFilterCategory,
|
||||||
categoryId,
|
categoryId,
|
||||||
isSubCategory,
|
isSubCategory,
|
||||||
brandId,
|
brandId,
|
||||||
collectionId,
|
collectionId,
|
||||||
fetchParams,
|
channelId,
|
||||||
|
selectedFilterBrand,
|
||||||
|
minPrice,
|
||||||
|
maxPrice,
|
||||||
|
sorting,
|
||||||
|
};
|
||||||
|
|
||||||
|
const requestId = ++activeRequestId.current;
|
||||||
|
setIsFetching(true);
|
||||||
|
|
||||||
|
const run = async () => {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
fetchCategoryPaginated,
|
fetchCategoryPaginated,
|
||||||
fetchBrandPaginated,
|
fetchBrandPaginated,
|
||||||
fetchCollectionPaginated,
|
fetchCollectionPaginated,
|
||||||
]);
|
fetchChannelPaginated,
|
||||||
|
} = fetchersRef.current; // ✅ ref'ten oku
|
||||||
|
|
||||||
useEffect(() => {
|
const params = {
|
||||||
const updateProducts = (newData, hasNextPage) => {
|
page: snapshot.currentPage,
|
||||||
if (!newData || newData.length === 0) {
|
perPage: 12,
|
||||||
if (currentPage === 1) {
|
brands: snapshot.selectedFilterBrand || undefined,
|
||||||
setProducts([]);
|
min_price: snapshot.minPrice || undefined,
|
||||||
setHasMore(false);
|
max_price: snapshot.maxPrice || undefined,
|
||||||
}
|
sorting: snapshot.sorting || undefined,
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setProducts((prev) => {
|
|
||||||
if (currentPage === 1) {
|
|
||||||
return newData;
|
|
||||||
}
|
|
||||||
|
|
||||||
const existingIds = new Set(prev.map((p) => p.id));
|
|
||||||
const newProducts = newData.filter((p) => !existingIds.has(p.id));
|
|
||||||
|
|
||||||
return newProducts.length > 0 ? [...prev, ...newProducts] : prev;
|
|
||||||
});
|
|
||||||
|
|
||||||
setHasMore(hasNextPage);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (paginatedCategoryProducts && shouldUseBaseQuery) {
|
let result = null;
|
||||||
updateProducts(
|
|
||||||
paginatedCategoryProducts.data || [],
|
if (snapshot.selectedFilterCategory) {
|
||||||
!!paginatedCategoryProducts.pagination?.next_page_url,
|
result = await fetchCategoryPaginated({
|
||||||
);
|
category: { id: snapshot.selectedFilterCategory, children: [] },
|
||||||
|
...params,
|
||||||
|
}).unwrap();
|
||||||
|
} else if (snapshot.categoryId && snapshot.isSubCategory) {
|
||||||
|
result = await fetchCategoryPaginated({
|
||||||
|
category: { id: parseInt(snapshot.categoryId), children: [] },
|
||||||
|
...params,
|
||||||
|
}).unwrap();
|
||||||
|
} else if (snapshot.brandId) {
|
||||||
|
result = await fetchBrandPaginated({
|
||||||
|
id: snapshot.brandId,
|
||||||
|
...params,
|
||||||
|
}).unwrap();
|
||||||
|
} else if (snapshot.collectionId) {
|
||||||
|
result = await fetchCollectionPaginated({
|
||||||
|
collectionId: snapshot.collectionId,
|
||||||
|
...params,
|
||||||
|
}).unwrap();
|
||||||
|
} else if (snapshot.channelId) {
|
||||||
|
result = await fetchChannelPaginated({
|
||||||
|
channelId: snapshot.channelId,
|
||||||
|
...params,
|
||||||
|
}).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestId !== activeRequestId.current) return;
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
setHasMore(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lazyCategoryProducts) {
|
const data = result.data || [];
|
||||||
updateProducts(
|
const hasNextPage =
|
||||||
lazyCategoryProducts.data || [],
|
result.pagination?.hasMorePages ||
|
||||||
lazyCategoryProducts.pagination?.hasMorePages || false,
|
!!result.pagination?.next_page_url ||
|
||||||
);
|
false;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Brand products
|
setProducts((prev) => {
|
||||||
if (paginatedBrandProducts) {
|
if (snapshot.currentPage === 1) return data;
|
||||||
updateProducts(
|
const existingIds = new Set(prev.map((p) => p.id));
|
||||||
paginatedBrandProducts.data || [],
|
const newItems = data.filter((p) => !existingIds.has(p.id));
|
||||||
!!paginatedBrandProducts.pagination?.next_page_url,
|
return newItems.length > 0 ? [...prev, ...newItems] : prev;
|
||||||
);
|
});
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (paginatedCollectionProducts) {
|
setHasMore(data.length > 0 ? hasNextPage : false);
|
||||||
updateProducts(
|
} catch (err) {
|
||||||
paginatedCollectionProducts.data || [],
|
if (requestId !== activeRequestId.current) return;
|
||||||
!!paginatedCollectionProducts.pagination?.next_page_url,
|
console.error("Fetch error:", err);
|
||||||
);
|
setHasMore(false);
|
||||||
|
} finally {
|
||||||
|
if (requestId === activeRequestId.current) {
|
||||||
|
setIsFetching(false);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
run();
|
||||||
}, [
|
}, [
|
||||||
paginatedCategoryProducts,
|
|
||||||
lazyCategoryProducts,
|
|
||||||
paginatedBrandProducts,
|
|
||||||
paginatedCollectionProducts,
|
|
||||||
currentPage,
|
|
||||||
shouldUseBaseQuery,
|
shouldUseBaseQuery,
|
||||||
|
searchQuery,
|
||||||
|
currentPage,
|
||||||
|
selectedFilterCategory,
|
||||||
|
categoryId,
|
||||||
|
isSubCategory,
|
||||||
|
brandId,
|
||||||
|
collectionId,
|
||||||
|
channelId,
|
||||||
|
selectedFilterBrand,
|
||||||
|
minPrice,
|
||||||
|
maxPrice,
|
||||||
|
sorting,
|
||||||
|
// ✅ fetcher fonksiyonlar dependency'den tamamen çıktı
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const isLoading =
|
const isLoading = shouldUseBaseQuery ? baseQueryFetching : isFetching;
|
||||||
categoryLoading ||
|
|
||||||
lazyCategoryLoading ||
|
|
||||||
brandPaginatedLoading ||
|
|
||||||
collectionPaginatedLoading ||
|
|
||||||
categoryFetching ||
|
|
||||||
lazyCategoryFetching ||
|
|
||||||
brandFetching ||
|
|
||||||
collectionFetching;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
products,
|
products,
|
||||||
@@ -318,3 +222,4 @@ const useCategoryProducts = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default useCategoryProducts;
|
export default useCategoryProducts;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useEffect, useState, useMemo, useRef } from "react";
|
import { useEffect, useState, useMemo, useRef } from "react";
|
||||||
import { useParams, useLocation, useNavigate } from "react-router-dom";
|
import { useParams, useLocation, useNavigate } from "react-router-dom";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -15,26 +13,56 @@ import CategoryFilters from "./components/CategoryFilters";
|
|||||||
import CategoryBreadcrumbs from "./components/CategoryBreadcrumbs";
|
import CategoryBreadcrumbs from "./components/CategoryBreadcrumbs";
|
||||||
import useCategoryData from "./hooks/useCategoryData";
|
import useCategoryData from "./hooks/useCategoryData";
|
||||||
import useCategoryProducts from "./hooks/useCategoryProducts";
|
import useCategoryProducts from "./hooks/useCategoryProducts";
|
||||||
|
import Carconfigurator from "../../components/CarConfigurator/Carconfigurator";
|
||||||
|
|
||||||
import MobilePhoneCard from "./components/Mobilephonecard";
|
import MobilePhoneCard from "./components/Mobilephonecard";
|
||||||
|
|
||||||
const CategoryPage = () => {
|
const CategoryPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { categoryId, collectionId, brandId } = useParams();
|
const { categoryId, collectionId, brandId, channelId } = useParams();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [pageState, setPageState] = useState({
|
const routeKey = useMemo(
|
||||||
|
() => `${categoryId || "x"}-${collectionId || "x"}-${brandId || "x"}-${channelId || "x"}`,
|
||||||
|
[categoryId, collectionId, brandId, channelId],
|
||||||
|
);
|
||||||
|
|
||||||
|
const getSavedState = (key, defaultVal) => {
|
||||||
|
if (location.state?.clearFilters) {
|
||||||
|
return defaultVal;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const saved = sessionStorage.getItem(`category_${key}_${routeKey}`);
|
||||||
|
if (saved) return JSON.parse(saved);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
return defaultVal;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSavedStateByKey = (route, key) => {
|
||||||
|
try {
|
||||||
|
const saved = sessionStorage.getItem(`category_${key}_${route}`);
|
||||||
|
if (saved) return JSON.parse(saved);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const [pageState, setPageState] = useState(() => getSavedState("pageState", {
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
minPrice: "",
|
minPrice: "",
|
||||||
maxPrice: "",
|
maxPrice: "",
|
||||||
});
|
sorting: "",
|
||||||
|
}));
|
||||||
|
|
||||||
const [filterState, setFilterState] = useState({
|
const [filterState, setFilterState] = useState(() => getSavedState("filterState", {
|
||||||
selectedFilterCategory: null,
|
selectedFilterCategory: null,
|
||||||
selectedFilterBrand: null,
|
selectedFilterBrand: null,
|
||||||
brandSearchQuery: "",
|
brandSearchQuery: "",
|
||||||
});
|
}));
|
||||||
|
|
||||||
const [isFilterDrawerOpen, setIsFilterDrawerOpen] = useState(false);
|
const [isFilterDrawerOpen, setIsFilterDrawerOpen] = useState(false);
|
||||||
const [windowWidth, setWindowWidth] = useState(window.innerWidth);
|
const [windowWidth, setWindowWidth] = useState(window.innerWidth);
|
||||||
@@ -45,17 +73,12 @@ const CategoryPage = () => {
|
|||||||
return () => window.removeEventListener("resize", handleResize);
|
return () => window.removeEventListener("resize", handleResize);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const routeKey = useMemo(
|
|
||||||
() => `${categoryId || "x"}-${collectionId || "x"}-${brandId || "x"}`,
|
|
||||||
[categoryId, collectionId, brandId]
|
|
||||||
);
|
|
||||||
|
|
||||||
const prevRouteRef = useRef(routeKey);
|
const prevRouteRef = useRef(routeKey);
|
||||||
const isInitialMount = useRef(true);
|
const isInitialMount = useRef(true);
|
||||||
|
|
||||||
const searchResults = useMemo(
|
const searchResults = useMemo(
|
||||||
() => location.state?.searchData?.data || [],
|
() => location.state?.searchData?.data || [],
|
||||||
[location.state?.searchData?.data]
|
[location.state?.searchData?.data],
|
||||||
);
|
);
|
||||||
const searchQuery = location.state?.searchQuery || null;
|
const searchQuery = location.state?.searchQuery || null;
|
||||||
|
|
||||||
@@ -65,6 +88,7 @@ const CategoryPage = () => {
|
|||||||
isSubCategory,
|
isSubCategory,
|
||||||
filtersData,
|
filtersData,
|
||||||
collectionData,
|
collectionData,
|
||||||
|
channelData,
|
||||||
isLoading: dataLoading,
|
isLoading: dataLoading,
|
||||||
hasError: dataError,
|
hasError: dataError,
|
||||||
fetchFilters,
|
fetchFilters,
|
||||||
@@ -72,6 +96,7 @@ const CategoryPage = () => {
|
|||||||
categoryId,
|
categoryId,
|
||||||
collectionId,
|
collectionId,
|
||||||
brandId,
|
brandId,
|
||||||
|
channelId,
|
||||||
selectedFilterCategory: filterState.selectedFilterCategory,
|
selectedFilterCategory: filterState.selectedFilterCategory,
|
||||||
searchQuery,
|
searchQuery,
|
||||||
});
|
});
|
||||||
@@ -85,6 +110,7 @@ const CategoryPage = () => {
|
|||||||
} = useCategoryProducts({
|
} = useCategoryProducts({
|
||||||
categoryId,
|
categoryId,
|
||||||
collectionId,
|
collectionId,
|
||||||
|
channelId,
|
||||||
brandId,
|
brandId,
|
||||||
selectedCategory,
|
selectedCategory,
|
||||||
isSubCategory,
|
isSubCategory,
|
||||||
@@ -93,7 +119,10 @@ const CategoryPage = () => {
|
|||||||
selectedFilterBrand: filterState.selectedFilterBrand,
|
selectedFilterBrand: filterState.selectedFilterBrand,
|
||||||
minPrice: pageState.minPrice,
|
minPrice: pageState.minPrice,
|
||||||
maxPrice: pageState.maxPrice,
|
maxPrice: pageState.maxPrice,
|
||||||
|
sorting: pageState.sorting,
|
||||||
searchQuery,
|
searchQuery,
|
||||||
|
initialProducts: getSavedState("products", []),
|
||||||
|
initialHasMore: getSavedState("hasMore", true),
|
||||||
});
|
});
|
||||||
const isMobilePhoneView =
|
const isMobilePhoneView =
|
||||||
(Number(categoryId) === 531 ||
|
(Number(categoryId) === 531 ||
|
||||||
@@ -103,21 +132,51 @@ const CategoryPage = () => {
|
|||||||
if (isInitialMount.current) {
|
if (isInitialMount.current) {
|
||||||
isInitialMount.current = false;
|
isInitialMount.current = false;
|
||||||
prevRouteRef.current = routeKey;
|
prevRouteRef.current = routeKey;
|
||||||
|
const savedScroll = getSavedState("scroll", 0);
|
||||||
|
if (savedScroll > 0) {
|
||||||
|
setTimeout(() => window.scrollTo(0, savedScroll), 100);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prevRouteRef.current === routeKey) return;
|
if (prevRouteRef.current === routeKey && !location.state?.clearFilters) return;
|
||||||
|
|
||||||
prevRouteRef.current = routeKey;
|
prevRouteRef.current = routeKey;
|
||||||
|
|
||||||
|
const shouldClear = location.state?.clearFilters;
|
||||||
|
|
||||||
|
const savedPageState = shouldClear ? null : getSavedStateByKey(routeKey, "pageState");
|
||||||
|
const savedFilterState = shouldClear ? null : getSavedStateByKey(routeKey, "filterState");
|
||||||
|
const savedProducts = shouldClear ? null : getSavedStateByKey(routeKey, "products");
|
||||||
|
const savedHasMore = shouldClear ? null : getSavedStateByKey(routeKey, "hasMore");
|
||||||
|
|
||||||
|
if (savedPageState && savedFilterState && savedProducts) {
|
||||||
|
setPageState(savedPageState);
|
||||||
|
setFilterState(savedFilterState);
|
||||||
|
setAllProducts(savedProducts);
|
||||||
|
setHasMore(savedHasMore ?? true);
|
||||||
|
const savedScroll = getSavedStateByKey(routeKey, "scroll");
|
||||||
|
if (savedScroll !== null) {
|
||||||
|
setTimeout(() => window.scrollTo(0, savedScroll), 100);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (prevRouteRef.current !== routeKey) {
|
||||||
setAllProducts([]);
|
setAllProducts([]);
|
||||||
setHasMore(true);
|
setHasMore(true);
|
||||||
setPageState({ currentPage: 1, minPrice: "", maxPrice: "" });
|
}
|
||||||
|
setPageState({
|
||||||
|
currentPage: 1,
|
||||||
|
minPrice: "",
|
||||||
|
maxPrice: "",
|
||||||
|
sorting: "",
|
||||||
|
});
|
||||||
setFilterState({
|
setFilterState({
|
||||||
selectedFilterCategory: null,
|
selectedFilterCategory: null,
|
||||||
selectedFilterBrand: null,
|
selectedFilterBrand: null,
|
||||||
brandSearchQuery: "",
|
brandSearchQuery: "",
|
||||||
});
|
});
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
if (location.state?.clearFilters) {
|
if (location.state?.clearFilters) {
|
||||||
navigate(location.pathname, { replace: true, state: {} });
|
navigate(location.pathname, { replace: true, state: {} });
|
||||||
@@ -125,11 +184,52 @@ const CategoryPage = () => {
|
|||||||
}, [
|
}, [
|
||||||
routeKey,
|
routeKey,
|
||||||
location.state?.clearFilters,
|
location.state?.clearFilters,
|
||||||
|
location.pathname,
|
||||||
navigate,
|
navigate,
|
||||||
setAllProducts,
|
setAllProducts,
|
||||||
setHasMore,
|
setHasMore,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const stateRef = useRef();
|
||||||
|
useEffect(() => {
|
||||||
|
stateRef.current = { routeKey, pageState, filterState, allProducts, hasMore };
|
||||||
|
}, [routeKey, pageState, filterState, allProducts, hasMore]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (stateRef.current) {
|
||||||
|
try {
|
||||||
|
const { routeKey: key, pageState: ps, filterState: fs, allProducts: ap, hasMore: hm } = stateRef.current;
|
||||||
|
sessionStorage.setItem(`category_pageState_${key}`, JSON.stringify(ps));
|
||||||
|
sessionStorage.setItem(`category_filterState_${key}`, JSON.stringify(fs));
|
||||||
|
sessionStorage.setItem(`category_products_${key}`, JSON.stringify(ap));
|
||||||
|
sessionStorage.setItem(`category_hasMore_${key}`, JSON.stringify(hm));
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Could not save category state to sessionStorage", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [pageState, filterState, allProducts, hasMore, routeKey]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let scrollTimeout;
|
||||||
|
const handleScroll = () => {
|
||||||
|
if (scrollTimeout) clearTimeout(scrollTimeout);
|
||||||
|
scrollTimeout = setTimeout(() => {
|
||||||
|
if (stateRef.current) {
|
||||||
|
try {
|
||||||
|
sessionStorage.setItem(`category_scroll_${stateRef.current.routeKey}`, JSON.stringify(window.scrollY));
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
};
|
||||||
|
window.addEventListener("scroll", handleScroll);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("scroll", handleScroll);
|
||||||
|
if (scrollTimeout) clearTimeout(scrollTimeout);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const filteredProducts = useMemo(() => {
|
const filteredProducts = useMemo(() => {
|
||||||
let list = searchQuery ? searchResults : allProducts;
|
let list = searchQuery ? searchResults : allProducts;
|
||||||
|
|
||||||
@@ -160,7 +260,7 @@ const CategoryPage = () => {
|
|||||||
if (filterState.selectedFilterCategory) {
|
if (filterState.selectedFilterCategory) {
|
||||||
const cat = findCategoryById(
|
const cat = findCategoryById(
|
||||||
categoriesData?.data,
|
categoriesData?.data,
|
||||||
filterState.selectedFilterCategory
|
filterState.selectedFilterCategory,
|
||||||
);
|
);
|
||||||
return cat?.name || "Category";
|
return cat?.name || "Category";
|
||||||
}
|
}
|
||||||
@@ -185,7 +285,12 @@ const CategoryPage = () => {
|
|||||||
selectedFilterBrand: null,
|
selectedFilterBrand: null,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
setPageState({ currentPage: 1, minPrice: "", maxPrice: "" });
|
setPageState((prev) => ({
|
||||||
|
currentPage: 1,
|
||||||
|
minPrice: "",
|
||||||
|
maxPrice: "",
|
||||||
|
sorting: prev.sorting,
|
||||||
|
}));
|
||||||
setAllProducts([]);
|
setAllProducts([]);
|
||||||
setHasMore(true);
|
setHasMore(true);
|
||||||
|
|
||||||
@@ -193,15 +298,15 @@ const CategoryPage = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleFilterCategoryDeselect = () => {
|
const handleFilterCategoryDeselect = () => {
|
||||||
setFilterState((prev) => ({
|
setFilterState((prev) => ({ ...prev, selectedFilterCategory: null }));
|
||||||
|
setPageState((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
selectedFilterCategory: null,
|
currentPage: 1,
|
||||||
|
minPrice: "",
|
||||||
|
maxPrice: "",
|
||||||
}));
|
}));
|
||||||
|
|
||||||
setPageState({ currentPage: 1, minPrice: "", maxPrice: "" });
|
|
||||||
setAllProducts([]);
|
setAllProducts([]);
|
||||||
setHasMore(true);
|
setHasMore(true);
|
||||||
|
|
||||||
if (categoryId) fetchFilters({ category_id: categoryId });
|
if (categoryId) fetchFilters({ category_id: categoryId });
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -211,32 +316,29 @@ const CategoryPage = () => {
|
|||||||
selectedFilterBrand: brandId,
|
selectedFilterBrand: brandId,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
setPageState({ currentPage: 1, minPrice: "", maxPrice: "" });
|
setPageState((prev) => ({
|
||||||
|
currentPage: 1,
|
||||||
|
minPrice: "",
|
||||||
|
maxPrice: "",
|
||||||
|
sorting: prev.sorting,
|
||||||
|
}));
|
||||||
setAllProducts([]);
|
setAllProducts([]);
|
||||||
setHasMore(true);
|
setHasMore(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFilterBrandDeselect = () => {
|
const handleFilterBrandDeselect = () => {
|
||||||
setFilterState((prev) => ({
|
setFilterState((prev) => ({ ...prev, selectedFilterBrand: null }));
|
||||||
|
setPageState((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
selectedFilterBrand: null,
|
currentPage: 1,
|
||||||
|
minPrice: "",
|
||||||
|
maxPrice: "",
|
||||||
}));
|
}));
|
||||||
|
|
||||||
setPageState({ currentPage: 1, minPrice: "", maxPrice: "" });
|
|
||||||
setAllProducts([]);
|
setAllProducts([]);
|
||||||
setHasMore(true);
|
setHasMore(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCategoryClick = (targetId) => {
|
const handleCategoryClick = (targetId) => {
|
||||||
setFilterState({
|
|
||||||
selectedFilterCategory: null,
|
|
||||||
selectedFilterBrand: null,
|
|
||||||
brandSearchQuery: "",
|
|
||||||
});
|
|
||||||
setPageState({ currentPage: 1, minPrice: "", maxPrice: "" });
|
|
||||||
setAllProducts([]);
|
|
||||||
setHasMore(true);
|
|
||||||
|
|
||||||
navigate(`/category/${targetId}`, {
|
navigate(`/category/${targetId}`, {
|
||||||
replace: false,
|
replace: false,
|
||||||
state: { clearFilters: true, timestamp: Date.now() },
|
state: { clearFilters: true, timestamp: Date.now() },
|
||||||
@@ -272,6 +374,21 @@ const CategoryPage = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.categoryPage}>
|
<div className={styles.categoryPage}>
|
||||||
|
{channelId && channelData ? (
|
||||||
|
<div className={styles.channelHeader}>
|
||||||
|
{channelData.media?.[0]?.thumbnail && (
|
||||||
|
<img
|
||||||
|
src={channelData.media[0].thumbnail}
|
||||||
|
alt={channelData.name}
|
||||||
|
className={styles.channelLogo}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className={styles.channelInfo}>
|
||||||
|
<h1>{channelData.name}</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
{(categoryId || filterState.selectedFilterCategory) && (
|
{(categoryId || filterState.selectedFilterCategory) && (
|
||||||
<CategoryBreadcrumbs
|
<CategoryBreadcrumbs
|
||||||
categoriesData={categoriesData}
|
categoriesData={categoriesData}
|
||||||
@@ -284,6 +401,8 @@ const CategoryPage = () => {
|
|||||||
<p className={styles.sum}>
|
<p className={styles.sum}>
|
||||||
{t("category.total")}: {totalItems} {t("category.items")}
|
{t("category.total")}: {totalItems} {t("category.items")}
|
||||||
</p>
|
</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className={styles.bars}>
|
<div className={styles.bars}>
|
||||||
<button
|
<button
|
||||||
@@ -310,22 +429,22 @@ const CategoryPage = () => {
|
|||||||
minPrice={pageState.minPrice}
|
minPrice={pageState.minPrice}
|
||||||
maxPrice={pageState.maxPrice}
|
maxPrice={pageState.maxPrice}
|
||||||
onMinPriceChange={(value) => {
|
onMinPriceChange={(value) => {
|
||||||
|
setAllProducts([]);
|
||||||
|
setHasMore(true);
|
||||||
setPageState((prev) => ({
|
setPageState((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
minPrice: value,
|
minPrice: value,
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
}));
|
}));
|
||||||
setAllProducts([]);
|
|
||||||
setHasMore(true);
|
|
||||||
}}
|
}}
|
||||||
onMaxPriceChange={(value) => {
|
onMaxPriceChange={(value) => {
|
||||||
|
setAllProducts([]);
|
||||||
|
setHasMore(true);
|
||||||
setPageState((prev) => ({
|
setPageState((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
maxPrice: value,
|
maxPrice: value,
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
}));
|
}));
|
||||||
setAllProducts([]);
|
|
||||||
setHasMore(true);
|
|
||||||
}}
|
}}
|
||||||
onCategorySelect={handleFilterCategorySelect}
|
onCategorySelect={handleFilterCategorySelect}
|
||||||
onCategoryDeselect={handleFilterCategoryDeselect}
|
onCategoryDeselect={handleFilterCategoryDeselect}
|
||||||
@@ -334,6 +453,15 @@ const CategoryPage = () => {
|
|||||||
onBrandSearchChange={(query) =>
|
onBrandSearchChange={(query) =>
|
||||||
setFilterState((prev) => ({ ...prev, brandSearchQuery: query }))
|
setFilterState((prev) => ({ ...prev, brandSearchQuery: query }))
|
||||||
}
|
}
|
||||||
|
sorting={pageState.sorting}
|
||||||
|
onSortingChange={(value) => {
|
||||||
|
setPageState((prev) => {
|
||||||
|
const newSorting = prev.sorting === value ? "" : value;
|
||||||
|
setAllProducts([]); // her zaman sıfırla
|
||||||
|
setHasMore(true);
|
||||||
|
return { ...prev, sorting: newSorting, currentPage: 1 };
|
||||||
|
});
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
|
||||||
@@ -348,22 +476,22 @@ const CategoryPage = () => {
|
|||||||
minPrice={pageState.minPrice}
|
minPrice={pageState.minPrice}
|
||||||
maxPrice={pageState.maxPrice}
|
maxPrice={pageState.maxPrice}
|
||||||
onMinPriceChange={(value) => {
|
onMinPriceChange={(value) => {
|
||||||
|
setAllProducts([]);
|
||||||
|
setHasMore(true);
|
||||||
setPageState((prev) => ({
|
setPageState((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
minPrice: value,
|
minPrice: value,
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
}));
|
}));
|
||||||
setAllProducts([]);
|
|
||||||
setHasMore(true);
|
|
||||||
}}
|
}}
|
||||||
onMaxPriceChange={(value) => {
|
onMaxPriceChange={(value) => {
|
||||||
|
setAllProducts([]);
|
||||||
|
setHasMore(true);
|
||||||
setPageState((prev) => ({
|
setPageState((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
maxPrice: value,
|
maxPrice: value,
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
}));
|
}));
|
||||||
setAllProducts([]);
|
|
||||||
setHasMore(true);
|
|
||||||
}}
|
}}
|
||||||
onCategorySelect={handleFilterCategorySelect}
|
onCategorySelect={handleFilterCategorySelect}
|
||||||
onCategoryDeselect={handleFilterCategoryDeselect}
|
onCategoryDeselect={handleFilterCategoryDeselect}
|
||||||
@@ -372,9 +500,22 @@ const CategoryPage = () => {
|
|||||||
onBrandSearchChange={(query) =>
|
onBrandSearchChange={(query) =>
|
||||||
setFilterState((prev) => ({ ...prev, brandSearchQuery: query }))
|
setFilterState((prev) => ({ ...prev, brandSearchQuery: query }))
|
||||||
}
|
}
|
||||||
|
sorting={pageState.sorting}
|
||||||
|
onSortingChange={(value) => {
|
||||||
|
setPageState((prev) => {
|
||||||
|
const newSorting = prev.sorting === value ? "" : value;
|
||||||
|
setAllProducts([]); // her zaman sıfırla
|
||||||
|
setHasMore(true);
|
||||||
|
return { ...prev, sorting: newSorting, currentPage: 1 };
|
||||||
|
});
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<main className={styles.productsContainer}>
|
<main className={styles.productsContainer}>
|
||||||
|
{(Number(categoryId) === 1136 ||
|
||||||
|
Number(filterState.selectedFilterCategory) === 1136) && (
|
||||||
|
<Carconfigurator />
|
||||||
|
)}
|
||||||
{isInitialLoad ? (
|
{isInitialLoad ? (
|
||||||
<div className={styles.loaderContainer}>
|
<div className={styles.loaderContainer}>
|
||||||
<Loader />
|
<Loader />
|
||||||
@@ -385,8 +526,10 @@ const CategoryPage = () => {
|
|||||||
next={loadMoreData}
|
next={loadMoreData}
|
||||||
hasMore={hasMore}
|
hasMore={hasMore}
|
||||||
scrollThreshold={0.8}
|
scrollThreshold={0.8}
|
||||||
|
scrollableTarget={null}
|
||||||
|
style={{ overflow: "hidden" }}
|
||||||
loader={
|
loader={
|
||||||
<div className={styles.loaderContainer}>
|
<div className={`${styles.loaderContainer} `}>
|
||||||
<Loader />
|
<Loader />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -409,7 +552,7 @@ const CategoryPage = () => {
|
|||||||
showFavoriteButton
|
showFavoriteButton
|
||||||
showAddToCart
|
showAddToCart
|
||||||
/>
|
/>
|
||||||
)
|
),
|
||||||
)}
|
)}
|
||||||
</InfiniteScroll>
|
</InfiniteScroll>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -11,12 +11,12 @@ const DeliveryTerms = () => {
|
|||||||
<p>Eltip bermek hyzmaty Aşgabat şäheriniň çägi bilen bir hatarda Büzmeýine we Änew şäherine hem elýeterlidir;</p>
|
<p>Eltip bermek hyzmaty Aşgabat şäheriniň çägi bilen bir hatarda Büzmeýine we Änew şäherine hem elýeterlidir;</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.termItem}>
|
{/* <div className={styles.termItem}>
|
||||||
<p>
|
<p>
|
||||||
Sargydyň iň pes çägi <span className={styles.highlight}>50 manat</span> bolmaly;
|
Sargydyň iň pes çägi <span className={styles.highlight}>50 manat</span> bolmaly;
|
||||||
sargydyňyz <span className={styles.highlight}>150 manatdan</span> geçse eltip bermek hyzmaty mugt;
|
sargydyňyz <span className={styles.highlight}>150 manatdan</span> geçse eltip bermek hyzmaty mugt;
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div> */}
|
||||||
|
|
||||||
<div className={styles.termItem}>
|
<div className={styles.termItem}>
|
||||||
<p>Saýtdan sargyt edeniňizden soňra operator size jaň edip sargyt tassyklar (eger hemişelik müşderi bolsaňyz sargytlaryňyz islegiňize göra awtomatik usulda hem tassyklanýar);</p>
|
<p>Saýtdan sargyt edeniňizden soňra operator size jaň edip sargyt tassyklar (eger hemişelik müşderi bolsaňyz sargytlaryňyz islegiňize göra awtomatik usulda hem tassyklanýar);</p>
|
||||||
|
|||||||
@@ -127,6 +127,7 @@
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
@media screen and (max-width: 640px) {
|
@media screen and (max-width: 640px) {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
@@ -140,6 +141,7 @@
|
|||||||
.row {
|
.row {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
|
gap: 12px;
|
||||||
@media screen and (max-width: 640px) {
|
@media screen and (max-width: 640px) {
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
@@ -157,10 +159,10 @@
|
|||||||
&:first-child {
|
&:first-child {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #000;
|
color: #000;
|
||||||
width: 25%;
|
// width: 25%;
|
||||||
@media screen and (max-width: 640px) {
|
// @media screen and (max-width: 640px) {
|
||||||
width: 50%;
|
// width: 50%;
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
@@ -295,3 +297,106 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pendingPriceBadgeWrapper {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pendingPriceBadge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #faeeda;
|
||||||
|
border: 0.5px solid #ef9f27;
|
||||||
|
color: #854f0b;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pendingPriceTooltip {
|
||||||
|
position: absolute;
|
||||||
|
bottom: calc(100% + 6px);
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background: var(--color-background-primary, #ffffff);
|
||||||
|
border: 0.5px solid var(--color-border-secondary, #e2e2e2);
|
||||||
|
border-radius: var(--border-radius-md, 6px);
|
||||||
|
padding: 8px 12px;
|
||||||
|
width: 220px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--color-text-primary, #333333);
|
||||||
|
line-height: 1.5;
|
||||||
|
z-index: 100;
|
||||||
|
white-space: normal;
|
||||||
|
pointer-events: none;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
strong {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
color: var(--color-text-primary, #000000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:global {
|
||||||
|
.pending-price-modal {
|
||||||
|
.ant-modal-content {
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-modal-header {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
.ant-modal-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-modal-body {
|
||||||
|
p {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #555;
|
||||||
|
margin: 0;
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-modal-footer {
|
||||||
|
margin-top: 20px;
|
||||||
|
.ant-btn-primary {
|
||||||
|
background-color: #888888;
|
||||||
|
border-color: #888888;
|
||||||
|
border-radius: 6px;
|
||||||
|
height: 36px;
|
||||||
|
padding: 0 20px;
|
||||||
|
font-weight: 500;
|
||||||
|
&:hover {
|
||||||
|
background-color: #666666;
|
||||||
|
border-color: #666666;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,63 +1,63 @@
|
|||||||
import { useParams } from "react-router-dom";
|
import { useParams, useNavigate } from "react-router-dom";
|
||||||
import styles from "./OrderDetail.module.scss";
|
import styles from "./OrderDetail.module.scss";
|
||||||
import { Ban, CircleCheck, X } from "lucide-react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useGetOrderByIdQuery } from "../../app/api/orderApi"; // Update with your correct path
|
import { useGetOrderByIdQuery } from "../../app/api/orderApi";
|
||||||
import track from "../../assets/track.jpg"; // Keep for delivery service icon
|
import track from "../../assets/track.jpg";
|
||||||
import Loader from "../../components/Loader/index";
|
import Loader from "../../components/Loader/index";
|
||||||
import { Result, Button } from "antd";
|
import { Result, Button } from "antd";
|
||||||
import { useNavigate } from "react-router-dom";
|
import PendingPriceBadge from "../../components/PendingPriceBadge";
|
||||||
|
|
||||||
|
const isPriceZero = (price) => !price || parseFloat(price) === 0;
|
||||||
|
|
||||||
const OrderDetail = () => {
|
const OrderDetail = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { id } = useParams(); // Get the order ID from URL params
|
const { id } = useParams();
|
||||||
const { data: orderData, isLoading, error } = useGetOrderByIdQuery(id);
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
// Format date function
|
const { data: orderData, isLoading, error } = useGetOrderByIdQuery(id);
|
||||||
|
|
||||||
const formatDate = (dateString) => {
|
const formatDate = (dateString) => {
|
||||||
try {
|
try {
|
||||||
const date = new Date(dateString);
|
return new Date(dateString).toLocaleString("tk-TM", {
|
||||||
return date.toLocaleString("tk-TM", {
|
|
||||||
day: "2-digit",
|
day: "2-digit",
|
||||||
month: "2-digit",
|
month: "2-digit",
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
hour: "2-digit",
|
hour: "2-digit",
|
||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch {
|
||||||
return dateString;
|
return dateString;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Format delivery time for display
|
|
||||||
const formatDeliveryTime = (time, date) => {
|
const formatDeliveryTime = (time, date) => {
|
||||||
try {
|
try {
|
||||||
const deliveryDate = new Date(date);
|
const formatted = new Date(date).toLocaleDateString("tk-TM", {
|
||||||
const formattedDate = deliveryDate.toLocaleDateString("tk-TM", {
|
|
||||||
day: "2-digit",
|
day: "2-digit",
|
||||||
month: "2-digit",
|
month: "2-digit",
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
});
|
});
|
||||||
return `${time} (${formattedDate})`;
|
return `${time} (${formatted})`;
|
||||||
} catch (e) {
|
} catch {
|
||||||
return `${time}`;
|
return time;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Calculate total order amount
|
|
||||||
const calculateTotal = (orderItems) => {
|
const calculateTotal = (orderItems) => {
|
||||||
if (!orderItems || !orderItems.length) return 0;
|
if (!orderItems?.length) return null;
|
||||||
|
const hasZero = orderItems.some((item) =>
|
||||||
|
isPriceZero(item.unit_price_amount),
|
||||||
|
);
|
||||||
|
if (hasZero) return null;
|
||||||
return orderItems
|
return orderItems
|
||||||
.reduce(
|
.reduce(
|
||||||
(sum, item) => sum + parseFloat(item.unit_price_amount) * item.quantity,
|
(sum, item) => sum + parseFloat(item.unit_price_amount) * item.quantity,
|
||||||
0
|
0,
|
||||||
)
|
)
|
||||||
.toFixed(2);
|
.toFixed(2);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle loading state
|
|
||||||
if (isLoading) return <Loader />;
|
if (isLoading) return <Loader />;
|
||||||
|
|
||||||
// Handle error state
|
|
||||||
if (error)
|
if (error)
|
||||||
return (
|
return (
|
||||||
<Result
|
<Result
|
||||||
@@ -72,10 +72,8 @@ const OrderDetail = () => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
// Handle case where order data is not available
|
|
||||||
if (!orderData) return <div className={styles.notFound}>Order not found</div>;
|
if (!orderData) return <div className={styles.notFound}>Order not found</div>;
|
||||||
|
|
||||||
// Calculate total
|
|
||||||
const totalAmount = calculateTotal(orderData.orderItems);
|
const totalAmount = calculateTotal(orderData.orderItems);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -84,39 +82,10 @@ const OrderDetail = () => {
|
|||||||
<h1>
|
<h1>
|
||||||
{t("order.orderNumber")}: {orderData.id}
|
{t("order.orderNumber")}: {orderData.id}
|
||||||
</h1>
|
</h1>
|
||||||
<div className={styles.Buttons}>
|
<div className={styles.Buttons} />
|
||||||
{/* <button className={styles.repeatButton}>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 512 512"
|
|
||||||
fill="currentColor"
|
|
||||||
>
|
|
||||||
<path d="M480 256c-17.67 0-32 14.31-32 32c0 52.94-43.06 96-96 96H192L192 344c0-9.469-5.578-18.06-14.23-21.94C169.1 318.3 159 319.8 151.9 326.2l-80 72C66.89 402.7 64 409.2 64 416s2.891 13.28 7.938 17.84l80 72C156.4 509.9 162.2 512 168 512c3.312 0 6.615-.6875 9.756-2.062C186.4 506.1 192 497.5 192 488L192 448h160c88.22 0 160-71.78 160-160C512 270.3 497.7 256 480 256zM160 128h159.1L320 168c0 9.469 5.578 18.06 14.23 21.94C337.4 191.3 340.7 192 343.1 192c5.812 0 11.57-2.125 16.07-6.156l80-72C445.1 109.3 448 102.8 448 95.1s-2.891-13.28-7.938-17.84l-80-72c-7.047-6.312-17.19-7.875-25.83-4.094C325.6 5.938 319.1 14.53 319.1 24L320 64H160C71.78 64 0 135.8 0 224c0 17.69 14.33 32 32 32s32-14.31 32-32C64 171.1 107.1 128 160 128z"></path>
|
|
||||||
</svg>{" "}
|
|
||||||
{t("order.repeatOrder")}
|
|
||||||
</button> */}
|
|
||||||
{/* <button className={styles.cancelButton}>
|
|
||||||
{" "}
|
|
||||||
<Ban />
|
|
||||||
{t("order.dropOrder")}
|
|
||||||
</button> */}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div className={styles.content}>
|
|
||||||
{/* Order Status */}
|
|
||||||
{/* <div className={styles.status}>
|
|
||||||
<p className={styles.statusText}>
|
|
||||||
<span className={styles.statusIcon}>
|
|
||||||
<CircleCheck />
|
|
||||||
</span>{" "}
|
|
||||||
{t("order.Your_order_has_been_accepted")}
|
|
||||||
</p>
|
|
||||||
<span className={styles.close}>
|
|
||||||
<X />
|
|
||||||
</span>
|
|
||||||
</div> */}
|
|
||||||
|
|
||||||
{/* Order Details */}
|
<div className={styles.content}>
|
||||||
<div className={styles.details}>
|
<div className={styles.details}>
|
||||||
<div className={styles.rowContainer}>
|
<div className={styles.rowContainer}>
|
||||||
<div className={styles.row}>
|
<div className={styles.row}>
|
||||||
@@ -132,7 +101,7 @@ const OrderDetail = () => {
|
|||||||
<span>
|
<span>
|
||||||
{formatDeliveryTime(
|
{formatDeliveryTime(
|
||||||
orderData.delivery_time,
|
orderData.delivery_time,
|
||||||
orderData.delivery_at
|
orderData.delivery_at,
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -144,11 +113,26 @@ const OrderDetail = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className={styles.row}>
|
<div className={styles.row}>
|
||||||
<span>{t("order.sum")}:</span>
|
<span>{t("order.sum")}:</span>
|
||||||
<span className={styles.total}>{totalAmount} m.</span>
|
<span className={styles.total}>
|
||||||
|
{totalAmount === null ? (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
display: "inline-flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 6,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PendingPriceBadge t={t} />
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
`${totalAmount} m.`
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Desktop table */}
|
||||||
<div className={styles.tableContainer}>
|
<div className={styles.tableContainer}>
|
||||||
<table className={styles.table}>
|
<table className={styles.table}>
|
||||||
<thead>
|
<thead>
|
||||||
@@ -165,7 +149,10 @@ const OrderDetail = () => {
|
|||||||
<tbody>
|
<tbody>
|
||||||
{orderData.orderItems.map((item, index) => {
|
{orderData.orderItems.map((item, index) => {
|
||||||
const product = item.product;
|
const product = item.product;
|
||||||
const itemTotal = (
|
const zeroPriceItem = isPriceZero(item.unit_price_amount);
|
||||||
|
const itemTotal = zeroPriceItem
|
||||||
|
? null
|
||||||
|
: (
|
||||||
parseFloat(item.unit_price_amount) * item.quantity
|
parseFloat(item.unit_price_amount) * item.quantity
|
||||||
).toFixed(2);
|
).toFixed(2);
|
||||||
|
|
||||||
@@ -181,27 +168,50 @@ const OrderDetail = () => {
|
|||||||
<td>{product.name}</td>
|
<td>{product.name}</td>
|
||||||
<td>{product.brand || "-"}</td>
|
<td>{product.brand || "-"}</td>
|
||||||
<td>{product.id || "-"}</td>
|
<td>{product.id || "-"}</td>
|
||||||
<td>{item.unit_price_amount} m.</td>
|
<td>
|
||||||
|
{zeroPriceItem ? (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
display: "inline-flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 6,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PendingPriceBadge />
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
`${item.unit_price_amount} m.`
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
<td>{item.quantity}</td>
|
<td>{item.quantity}</td>
|
||||||
<td>{itemTotal} m.</td>
|
<td>
|
||||||
|
{itemTotal === null ? (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
display: "inline-flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 6,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PendingPriceBadge />
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
`${itemTotal} m.`
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{/* Add delivery service row if shipping method exists */}
|
|
||||||
{orderData.shipping_method && (
|
{orderData.shipping_method && (
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<img
|
<img src={track} alt="Delivery" className={styles.image} />
|
||||||
src={track}
|
|
||||||
alt="Delivery Service"
|
|
||||||
className={styles.image}
|
|
||||||
/>
|
|
||||||
</td>
|
</td>
|
||||||
<td>Eltip bermek hyzmaty</td>
|
<td>Eltip bermek hyzmaty</td>
|
||||||
<td>Beýleki</td>
|
<td>Beýleki</td>
|
||||||
<td>DELIVERY</td>
|
<td>DELIVERY</td>
|
||||||
<td>10.00 m.</td>{" "}
|
<td>10.00 m.</td>
|
||||||
{/* You may need to get actual delivery cost from API */}
|
|
||||||
<td>1</td>
|
<td>1</td>
|
||||||
<td>10.00 m.</td>
|
<td>10.00 m.</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -210,13 +220,15 @@ const OrderDetail = () => {
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Mobile View */}
|
|
||||||
|
{/* Mobile cards */}
|
||||||
<div className={styles.productList}>
|
<div className={styles.productList}>
|
||||||
{orderData.orderItems.map((item, index) => {
|
{orderData.orderItems.map((item, index) => {
|
||||||
const product = item.product;
|
const product = item.product;
|
||||||
const itemTotal = (
|
const zeroPriceItem = isPriceZero(item.unit_price_amount);
|
||||||
parseFloat(item.unit_price_amount) * item.quantity
|
const itemTotal = zeroPriceItem
|
||||||
).toFixed(2);
|
? null
|
||||||
|
: (parseFloat(item.unit_price_amount) * item.quantity).toFixed(2);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.card} key={index}>
|
<div className={styles.card} key={index}>
|
||||||
@@ -233,18 +245,30 @@ const OrderDetail = () => {
|
|||||||
{t("order.quantity")}: {item.quantity}
|
{t("order.quantity")}: {item.quantity}
|
||||||
</span>
|
</span>
|
||||||
<span className={styles.price}>
|
<span className={styles.price}>
|
||||||
{item.unit_price_amount} m.
|
{zeroPriceItem ? (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
display: "inline-flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 6,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PendingPriceBadge />
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
`${item.unit_price_amount} m.`
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{/* Add delivery service card if shipping method exists */}
|
|
||||||
{orderData.shipping_method && (
|
{/* {orderData.shipping_method && (
|
||||||
<div className={styles.card}>
|
<div className={styles.card}>
|
||||||
<div className={styles.imageContainer}>
|
<div className={styles.imageContainer}>
|
||||||
<img src={track} alt="Delivery Service" />
|
<img src={track} alt="Delivery" />
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.detailsMobile}>
|
<div className={styles.detailsMobile}>
|
||||||
<h3 className={styles.title}>Beýleki</h3>
|
<h3 className={styles.title}>Beýleki</h3>
|
||||||
@@ -257,7 +281,7 @@ const OrderDetail = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)} */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// SargytlarymComponent.module.scss
|
// SargytlarymComponent.module.scss
|
||||||
.container {
|
.container {
|
||||||
padding: 15px 24px 0 24px;
|
padding: 15px 24px 24px 24px;
|
||||||
max-width: 1366px;
|
max-width: 1366px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@@ -121,3 +121,106 @@
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #888888;
|
color: #888888;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pendingPriceBadgeWrapper {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pendingPriceBadge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #faeeda;
|
||||||
|
border: 0.5px solid #ef9f27;
|
||||||
|
color: #854f0b;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pendingPriceTooltip {
|
||||||
|
position: absolute;
|
||||||
|
bottom: calc(100% + 6px);
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background: var(--color-background-primary, #ffffff);
|
||||||
|
border: 0.5px solid var(--color-border-secondary, #e2e2e2);
|
||||||
|
border-radius: var(--border-radius-md, 6px);
|
||||||
|
padding: 8px 12px;
|
||||||
|
width: 220px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--color-text-primary, #333333);
|
||||||
|
line-height: 1.5;
|
||||||
|
z-index: 100;
|
||||||
|
white-space: normal;
|
||||||
|
pointer-events: none;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
strong {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
color: var(--color-text-primary, #000000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:global {
|
||||||
|
.pending-price-modal {
|
||||||
|
.ant-modal-content {
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-modal-header {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
.ant-modal-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-modal-body {
|
||||||
|
p {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #555;
|
||||||
|
margin: 0;
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-modal-footer {
|
||||||
|
margin-top: 20px;
|
||||||
|
.ant-btn-primary {
|
||||||
|
background-color: #888888;
|
||||||
|
border-color: #888888;
|
||||||
|
border-radius: 6px;
|
||||||
|
height: 36px;
|
||||||
|
padding: 0 20px;
|
||||||
|
font-weight: 500;
|
||||||
|
&:hover {
|
||||||
|
background-color: #666666;
|
||||||
|
border-color: #666666;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,36 +1,40 @@
|
|||||||
// Orders.jsx
|
import React, { useState } from "react";
|
||||||
import React from "react";
|
|
||||||
import styles from "./Orders.module.scss";
|
import styles from "./Orders.module.scss";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useGetOrdersQuery } from "../../app/api/orderApi"; // Update with your correct path
|
import { useGetOrdersQuery } from "../../app/api/orderApi";
|
||||||
import EmptyOrderState from "./emptyOrder"; // Import the EmptyOrderState component
|
import EmptyOrderState from "./emptyOrder";
|
||||||
import Loader from "../../components/Loader/index";
|
import Loader from "../../components/Loader/index";
|
||||||
import { Result, Button } from "antd";
|
import { Result, Button } from "antd";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import PendingPriceBadge from "../../components/PendingPriceBadge";
|
||||||
|
|
||||||
|
const isPriceZero = (price) => !price || parseFloat(price) === 0;
|
||||||
|
|
||||||
|
const orderHasZeroPrice = (orderItems) =>
|
||||||
|
orderItems?.some((item) => isPriceZero(item.unit_price_amount));
|
||||||
|
|
||||||
const Orders = () => {
|
const Orders = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { data: orders, isLoading, error } = useGetOrdersQuery();
|
const { data: orders, isLoading, error } = useGetOrdersQuery();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
// Function to format date - implement this or use a library like date-fns
|
|
||||||
const formatOrderDate = (dateString) => {
|
const formatOrderDate = (dateString) => {
|
||||||
try {
|
try {
|
||||||
const date = new Date(dateString);
|
return new Date(dateString).toLocaleString("tk-TM", {
|
||||||
return date.toLocaleString("tk-TM", {
|
|
||||||
day: "2-digit",
|
day: "2-digit",
|
||||||
month: "2-digit",
|
month: "2-digit",
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
hour: "2-digit",
|
hour: "2-digit",
|
||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch {
|
||||||
return dateString;
|
return dateString;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isLoading) return <Loader />;
|
if (isLoading) return <Loader />;
|
||||||
|
|
||||||
// Handle error state
|
|
||||||
if (error)
|
if (error)
|
||||||
return (
|
return (
|
||||||
<Result
|
<Result
|
||||||
@@ -45,16 +49,13 @@ const Orders = () => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
// Handle empty orders - render EmptyOrderState component
|
if (!orders || orders.length === 0) return <EmptyOrderState />;
|
||||||
if (!orders || orders.length === 0) {
|
|
||||||
return <EmptyOrderState />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<h2 className={styles.title}>Sargytlarym</h2>
|
<h2 className={styles.title}>Sargytlarym</h2>
|
||||||
|
|
||||||
{/* Desktop table view */}
|
{/* Desktop table */}
|
||||||
<div className={styles.tableContainer}>
|
<div className={styles.tableContainer}>
|
||||||
<table className={styles.table}>
|
<table className={styles.table}>
|
||||||
<thead>
|
<thead>
|
||||||
@@ -69,11 +70,11 @@ const Orders = () => {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{orders.map((order) => {
|
{orders.map((order) => {
|
||||||
// Calculate total order amount
|
const hasZeroPrice = orderHasZeroPrice(order.orderItems);
|
||||||
const totalAmount = order.orderItems.reduce(
|
const totalAmount = order.orderItems.reduce(
|
||||||
(sum, item) =>
|
(sum, item) =>
|
||||||
sum + parseFloat(item.unit_price_amount) * item.quantity,
|
sum + parseFloat(item.unit_price_amount) * item.quantity,
|
||||||
0
|
0,
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -81,7 +82,19 @@ const Orders = () => {
|
|||||||
<td>{order.id}</td>
|
<td>{order.id}</td>
|
||||||
<td>{formatOrderDate(order.delivery_at)}</td>
|
<td>{formatOrderDate(order.delivery_at)}</td>
|
||||||
<td style={{ color: "#888888", fontWeight: "700" }}>
|
<td style={{ color: "#888888", fontWeight: "700" }}>
|
||||||
{totalAmount.toFixed(2)} m.
|
{hasZeroPrice ? (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
display: "inline-flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 6,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("cart.pendingPriceTitle")} <PendingPriceBadge />
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
`${totalAmount.toFixed(2)} m.`
|
||||||
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td>{order.payment_type}</td>
|
<td>{order.payment_type}</td>
|
||||||
<td>{order.status}</td>
|
<td>{order.status}</td>
|
||||||
@@ -99,22 +112,35 @@ const Orders = () => {
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Mobile card view */}
|
{/* Mobile cards */}
|
||||||
<div className={styles.Mobilecontainer}>
|
<div className={styles.Mobilecontainer}>
|
||||||
{orders.map((order) => {
|
{orders.map((order) => {
|
||||||
|
const hasZeroPrice = orderHasZeroPrice(order.orderItems);
|
||||||
const totalAmount = order.orderItems.reduce(
|
const totalAmount = order.orderItems.reduce(
|
||||||
(sum, item) =>
|
(sum, item) =>
|
||||||
sum + parseFloat(item.unit_price_amount) * item.quantity,
|
sum + parseFloat(item.unit_price_amount) * item.quantity,
|
||||||
0
|
0,
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link to={`/orderdetail/${order.id}`} key={order.id}>
|
<div
|
||||||
<div className={styles.orderCard}>
|
key={order.id}
|
||||||
|
className={styles.orderCard}
|
||||||
|
onClick={(e) => {
|
||||||
|
// Modal veya badge içerisine tıklandığında yönlendirmeyi engelle
|
||||||
|
if (
|
||||||
|
e.target.closest(`.${styles.pendingPriceBadgeWrapper}`) ||
|
||||||
|
e.target.closest(".ant-modal-root") ||
|
||||||
|
e.target.closest(".ant-modal-wrap")
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
navigate(`/orderdetail/${order.id}`);
|
||||||
|
}}
|
||||||
|
style={{ cursor: "pointer" }}
|
||||||
|
>
|
||||||
<div className={styles.orderRow}>
|
<div className={styles.orderRow}>
|
||||||
<span className={styles.label}>
|
<span className={styles.label}>{t("order.orderNumber")}:</span>
|
||||||
{t("order.orderNumber")}:
|
|
||||||
</span>
|
|
||||||
<span className={styles.value}>{order.id}</span>
|
<span className={styles.value}>{order.id}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.orderRow}>
|
<div className={styles.orderRow}>
|
||||||
@@ -126,7 +152,19 @@ const Orders = () => {
|
|||||||
<div className={styles.orderRow}>
|
<div className={styles.orderRow}>
|
||||||
<span className={styles.label}>{t("order.sum")}:</span>
|
<span className={styles.label}>{t("order.sum")}:</span>
|
||||||
<span className={styles.total}>
|
<span className={styles.total}>
|
||||||
{totalAmount.toFixed(2)} m.
|
{hasZeroPrice ? (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
display: "inline-flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 6,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("cart.pendingPriceTitle")} <PendingPriceBadge />
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
`${totalAmount.toFixed(2)} m.`
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.orderRow}>
|
<div className={styles.orderRow}>
|
||||||
@@ -136,13 +174,10 @@ const Orders = () => {
|
|||||||
<span className={styles.value}>{order.payment_type}</span>
|
<span className={styles.value}>{order.payment_type}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.orderRow}>
|
<div className={styles.orderRow}>
|
||||||
<span className={styles.label}>
|
<span className={styles.label}>{t("order.orderStatus")}:</span>
|
||||||
{t("order.orderStatus")}:
|
|
||||||
</span>
|
|
||||||
<span className={styles.value}>{order.status}</span>
|
<span className={styles.value}>{order.status}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Breadcrumb ───────────────────────────────────────────────────
|
||||||
.breadcrumb {
|
.breadcrumb {
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
color: #666;
|
color: #666;
|
||||||
@@ -19,76 +20,274 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Product section: 3 kolon ─────────────────────────────────────
|
||||||
|
// desktop: [image 35%] | [info+description flex:1] | [purchase 260px]
|
||||||
|
// tablet: [image 45%] [info 55%] / [purchase full-width]
|
||||||
|
// mobile: tek kolon
|
||||||
.productSection {
|
.productSection {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 24px;
|
gap: 24px;
|
||||||
background-color: rgb(255, 255, 255);
|
align-items: flex-start;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
// background-color: #fff;
|
||||||
|
// box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 1.25rem;
|
// padding: 1.25rem;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
@media screen and (max-width: 900px) {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 639px) {
|
@media screen and (max-width: 639px) {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 0.75rem;
|
border-radius: 8px;
|
||||||
|
// padding: 0.75rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Sol: resim kolonu ────────────────────────────────────────────
|
||||||
.productImage {
|
.productImage {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
padding: 20px;
|
// padding: 20px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
width: 40%;
|
width: 36%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
|
||||||
|
|
||||||
@media screen and (max-width: 900px) {
|
@media screen and (max-width: 900px) {
|
||||||
padding: 5px;
|
width: 45%;
|
||||||
|
border-radius: 8px;
|
||||||
|
// padding: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 639px) {
|
@media screen and (max-width: 639px) {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
padding: 0;
|
||||||
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
width: 99%;
|
width: 99%;
|
||||||
height: auto;
|
height: auto;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
// border: 1px solid #eee;
|
|
||||||
@media screen and (max-width: 900px) {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Orta: isim + meta + description kolonu ───────────────────────
|
||||||
.productInfo {
|
.productInfo {
|
||||||
width: 60%;
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
|
||||||
|
@media screen and (max-width: 900px) {
|
||||||
|
// tablet: image(45%) + info yan yana, purchase wrap ile alta iner
|
||||||
|
width: calc(55% - 24px);
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 639px) {
|
@media screen and (max-width: 639px) {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
|
||||||
@media screen and (max-width: 520px) {
|
// mobile'da purchase card orta kolona taşınır (sticky bar var)
|
||||||
border-bottom: 1px solid #e5e7eb;
|
border-bottom: 1px solid #e5e7eb;
|
||||||
}
|
}
|
||||||
|
|
||||||
.productTitle {
|
.productTitle {
|
||||||
font-size: 30px;
|
font-size: 24px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin-bottom: 12px;
|
margin: 0 0 4px;
|
||||||
color: #000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.productDescription {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #000;
|
color: #000;
|
||||||
margin-bottom: 24px;
|
line-height: 1.3;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.productMeta {
|
// ─── Sağ: satın alma kartı kolonu ────────────────────────────────
|
||||||
background: #f5f5f5;
|
.purchaseCol {
|
||||||
// padding: 16px;
|
width: 260px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
@media screen and (max-width: 900px) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 639px) {
|
||||||
|
display: none; // mobile'da sticky bar devreye girer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.purchaseCard {
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
margin-bottom: 24px;
|
padding: 16px;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Fiyat satırı ─────────────────────────────────────────────────
|
||||||
|
.priceRow {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
border-bottom: 1px solid #f3f4f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.priceLabel {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.priceRight {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oldPrice {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #d32824;
|
||||||
|
text-decoration: line-through;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Aksiyon butonları satırı ─────────────────────────────────────
|
||||||
|
.Btn {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.addToCartButton {
|
||||||
|
flex: 1;
|
||||||
|
height: 42px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: none;
|
||||||
|
background-color: #d32824;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 150ms ease;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
fill: #fff;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #e86064;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.favoriteButton {
|
||||||
|
width: 42px;
|
||||||
|
height: 42px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 6px;
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 150ms ease;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
fill: #888;
|
||||||
|
height: 20px;
|
||||||
|
width: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #d32824;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
fill: #d32824;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Quantity controls ────────────────────────────────────────────
|
||||||
|
.quantityControls {
|
||||||
|
flex: 1;
|
||||||
|
height: 42px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background-color: #d32824;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
span {
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 16px;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quantityBtn {
|
||||||
|
width: 42px;
|
||||||
|
height: 42px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
fill: #fff;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #e86064;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Meta tablo ───────────────────────────────────────────────────
|
||||||
|
.productMeta {
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
|
||||||
|
gap: 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
.metaItem {
|
.metaItem {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 8px 16px;
|
padding-bottom: 8px;
|
||||||
border-bottom: 2px solid #ffffff;
|
border-bottom: 1px solid #f1f1f1;
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
@@ -98,112 +297,180 @@
|
|||||||
.metaLabel {
|
.metaLabel {
|
||||||
color: #000;
|
color: #000;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.metaValue {
|
.metaValue {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 500;
|
font-weight: 600;
|
||||||
}
|
|
||||||
}
|
|
||||||
.Btn {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
@media screen and (max-width: 639px) {
|
|
||||||
width: 65%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.priceContainer {
|
|
||||||
display: flex;
|
|
||||||
align-items: baseline;
|
|
||||||
gap: 10px;
|
|
||||||
@media screen and (max-width: 639px) {
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 5px;
|
|
||||||
width: 35%;
|
|
||||||
align-items: center;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.productActions {
|
// ─── Description card ─────────────────────────────────────────────
|
||||||
|
.descriptionCard {
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.descriptionCard::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
height: 125px;
|
||||||
|
pointer-events: none;
|
||||||
|
background: linear-gradient(
|
||||||
|
to top,
|
||||||
|
rgba(0, 0, 0, 0.95) 0%,
|
||||||
|
rgba(0, 0, 0, 0.7) 0%,
|
||||||
|
rgba(0, 0, 0, 0.3) 35%,
|
||||||
|
rgba(255, 255, 255, 0) 35%
|
||||||
|
);
|
||||||
|
z-index: 2;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 300ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.descriptionCardCollapsed::after {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.productDescriptionCollapsed + div {
|
||||||
|
/* Read more butonunu gradient üstünde göstermek için z-index */
|
||||||
|
position: relative;
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.descriptionHeader {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
.descriptionIcon {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #1a1a2e;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
stroke: #fff;
|
||||||
|
fill: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.descriptionTitle {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #000;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.productDescription {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #000;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.productDescriptionCollapsed {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.productDescriptionWrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.readMoreBtn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 8px 0 0 0;
|
||||||
|
font-size: 16px;
|
||||||
|
display: inline-block;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
margin-top: 8px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 4;
|
||||||
|
transition: all 150ms ease;
|
||||||
|
text-shadow: 0 2px 6px rgba(0, 0, 0, 0.8), 0 1px 2px rgba(0, 0, 0, 0.6);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
opacity: 1;
|
||||||
|
text-shadow: 0 3px 10px rgba(0, 0, 0, 0.95), 0 1px 3px rgba(0, 0, 0, 0.7);
|
||||||
|
font-size: 17px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ─── Mobile sticky bar ────────────────────────────────────────────
|
||||||
|
.productActionsMobile {
|
||||||
|
display: none;
|
||||||
|
|
||||||
@media screen and (max-width: 639px) {
|
@media screen and (max-width: 639px) {
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.productActionsMobile {
|
|
||||||
@media screen and (min-width: 639px) {
|
|
||||||
background-color: #fff;
|
|
||||||
display: none !important;
|
|
||||||
position: sticky !important;
|
|
||||||
bottom: 60px !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.productActions,
|
|
||||||
.productActionsMobile {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
border-bottom: 1px solid #e5e7eb;
|
position: sticky;
|
||||||
|
bottom: 59px;
|
||||||
|
z-index: 50;
|
||||||
|
background: #fff;
|
||||||
border-top: 1px solid #e5e7eb;
|
border-top: 1px solid #e5e7eb;
|
||||||
background-color: #fff;
|
border-bottom: 1px solid #e5e7eb;
|
||||||
padding: 15px 16px;
|
padding: 10px 16px;
|
||||||
@media screen and (max-width: 520px) {
|
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.08);
|
||||||
border-bottom: none;
|
gap: 12px;
|
||||||
border-top: none;
|
border-radius: 8px;
|
||||||
padding: 0;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobilePriceContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
width: 45%;
|
||||||
|
|
||||||
.price {
|
.price {
|
||||||
font-size: 24px;
|
font-size: 20px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #000;
|
color: #000;
|
||||||
@media screen and (max-width: 520px) {
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.oldPrice {
|
.oldPrice {
|
||||||
font-size: 16px;
|
font-size: 13px;
|
||||||
color: #d32824;
|
color: #d32824;
|
||||||
text-decoration: line-through;
|
text-decoration: line-through;
|
||||||
font-weight: 600;
|
font-weight: 500;
|
||||||
@media screen and (max-width: 520px) {
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.favoriteButton {
|
.mobileBtnContainer {
|
||||||
height: 36px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
// margin-right: 0.5rem;
|
gap: 8px;
|
||||||
justify-content: center;
|
width: 55%;
|
||||||
align-items: center;
|
|
||||||
border-radius: 0.375rem;
|
|
||||||
background-color: rgb(255 255 255);
|
|
||||||
border: 1px solid rgb(237 228 255);
|
|
||||||
svg {
|
|
||||||
fill: #888888;
|
|
||||||
height: 20px;
|
|
||||||
width: 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.wishlistButton {
|
|
||||||
background: #fff;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: #f5f5f5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.productGrid {
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Similar products ─────────────────────────────────────────────
|
||||||
.similarProducts {
|
.similarProducts {
|
||||||
margin-top: 48px;
|
margin-top: 40px;
|
||||||
|
|
||||||
.sectionTitle {
|
.sectionTitle {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
@@ -216,117 +483,27 @@
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||||
gap: 24px;
|
gap: 24px;
|
||||||
|
|
||||||
@media screen and (max-width: 1230px) {
|
@media screen and (max-width: 1230px) {
|
||||||
grid-template-columns: repeat(auto-fill, minmax(225px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(225px, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 1023px) {
|
@media screen and (max-width: 1023px) {
|
||||||
grid-template-columns: repeat(auto-fill, minmax(228px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(228px, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 768px) {
|
@media screen and (max-width: 768px) {
|
||||||
grid-template-columns: repeat(auto-fill, minmax(234px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(234px, 1fr));
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
@media screen and (max-width: 767px) {
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
|
||||||
}
|
|
||||||
@media screen and (max-width: 510px) {
|
@media screen and (max-width: 510px) {
|
||||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
// ─── Misc ─────────────────────────────────────────────────────────
|
||||||
.productSection {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.addToCartButton {
|
|
||||||
// height: 40px;
|
|
||||||
display: flex;
|
|
||||||
padding-left: 0.5rem;
|
|
||||||
padding-right: 0.5rem;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
border-width: 1px;
|
|
||||||
width: 100%;
|
|
||||||
min-width: 158px;
|
|
||||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
transition-duration: 300ms;
|
|
||||||
transition-duration: 150ms;
|
|
||||||
background-color: #d32824;
|
|
||||||
border: none;
|
|
||||||
@media screen and (max-width: 639px) {
|
|
||||||
min-width: auto;
|
|
||||||
}
|
|
||||||
svg {
|
|
||||||
fill: #fff;
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: #e86064;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.quantityControls {
|
|
||||||
min-width: 158px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 2.5rem;
|
|
||||||
background-color: #d32824;
|
|
||||||
// width: 10rem;
|
|
||||||
// justify-content: center;
|
|
||||||
border-radius: 5px;
|
|
||||||
width: 100%;
|
|
||||||
@media screen and (max-width: 520px) {
|
|
||||||
min-width: auto;
|
|
||||||
gap: 0;
|
|
||||||
}
|
|
||||||
span {
|
|
||||||
color: #fff;
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 16px;
|
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quantityBtn {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
border: none;
|
|
||||||
background: transparent;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
svg {
|
|
||||||
fill: #fff;
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: #e86064;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.outOfStock {
|
|
||||||
background-color: #ff4d4f;
|
|
||||||
}
|
|
||||||
|
|
||||||
.disabled {
|
|
||||||
opacity: 0.5;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modalButton {
|
.modalButton {
|
||||||
// Style for modal buttons
|
|
||||||
padding: 6px 15px;
|
padding: 6px 15px;
|
||||||
background-color: #1890ff;
|
background-color: #1890ff;
|
||||||
color: white;
|
color: white;
|
||||||
@@ -334,3 +511,21 @@
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wishlistButton {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.outOfStock {
|
||||||
|
background-color: #ff4d4f;
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { useParams, useNavigate } from "react-router-dom";
|
import { useParams, useNavigate, Link } from "react-router-dom";
|
||||||
import styles from "./ProductPage.module.scss";
|
import styles from "./ProductPage.module.scss";
|
||||||
import { IoMdHeartEmpty, IoMdHeart } from "react-icons/io";
|
import { IoMdHeartEmpty, IoMdHeart } from "react-icons/io";
|
||||||
import { FaShoppingCart } from "react-icons/fa";
|
import { FaShoppingCart } from "react-icons/fa";
|
||||||
@@ -26,6 +26,10 @@ import {
|
|||||||
import ImageCarousel from "../../components/ProductCard/imageCarousel/index";
|
import ImageCarousel from "../../components/ProductCard/imageCarousel/index";
|
||||||
import Loader from "../../components/Loader/index";
|
import Loader from "../../components/Loader/index";
|
||||||
import { Result, Button } from "antd";
|
import { Result, Button } from "antd";
|
||||||
|
import { div } from "framer-motion/client";
|
||||||
|
import PendingPriceBadge from "../../components/PendingPriceBadge";
|
||||||
|
|
||||||
|
const isPriceZero = (price) => !price || parseFloat(price) === 0;
|
||||||
|
|
||||||
const ProductPage = ({
|
const ProductPage = ({
|
||||||
productProp,
|
productProp,
|
||||||
@@ -38,27 +42,107 @@ const ProductPage = ({
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { productId } = useParams();
|
const { productId } = useParams();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: productResponse,
|
data: productResponse,
|
||||||
error: productError,
|
error: productError,
|
||||||
isLoading: productLoading,
|
isLoading: productLoading,
|
||||||
} = useGetProductByIdQuery(productId);
|
} = useGetProductByIdQuery(productId);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: similarProductsResponse,
|
data: similarProductsResponse,
|
||||||
error: similarProductsError,
|
error: similarProductsError,
|
||||||
isLoading: similarProductsLoading,
|
isLoading: similarProductsLoading,
|
||||||
} = useGetRelatedProductsQuery(productId);
|
} = useGetRelatedProductsQuery(productId);
|
||||||
|
|
||||||
const product = productResponse?.data;
|
const product = productResponse?.data;
|
||||||
const similarProducts = similarProductsResponse?.data;
|
const similarProducts = similarProductsResponse?.data;
|
||||||
|
|
||||||
const [stockErrorModalVisible, setStockErrorModalVisible] = useState(false);
|
const [stockErrorModalVisible, setStockErrorModalVisible] = useState(false);
|
||||||
const [addFavorite] = useAddFavoriteMutation();
|
const [addFavorite] = useAddFavoriteMutation();
|
||||||
const [removeFavorite] = useRemoveFavoriteMutation();
|
const [removeFavorite] = useRemoveFavoriteMutation();
|
||||||
const { data: favoriteProducts = [] } = useGetFavoritesQuery();
|
const { data: favoriteProducts = [] } = useGetFavoritesQuery();
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [localIsFavorite, setLocalIsFavorite] = useState(
|
const [localIsFavorite, setLocalIsFavorite] = useState(
|
||||||
favoriteProducts.some((fav) => fav.product?.id === product?.id)
|
favoriteProducts.some((fav) => fav.product?.id === product?.id),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [isDescExpanded, setIsDescExpanded] = useState(false);
|
||||||
|
const [showReadMore, setShowReadMore] = useState(false);
|
||||||
|
const [collapsedMaxHeight, setCollapsedMaxHeight] = useState(null);
|
||||||
|
const descRef = React.useRef(null);
|
||||||
|
const productInfoRef = React.useRef(null);
|
||||||
|
const imageColRef = React.useRef(null);
|
||||||
|
|
||||||
|
// Ürün değişince desc'i kapat
|
||||||
|
useEffect(() => {
|
||||||
|
setIsDescExpanded(false);
|
||||||
|
}, [productId]);
|
||||||
|
|
||||||
|
// Resim kolonu yüksekliği ile desc kolonu yüksekliğini karşılaştır
|
||||||
|
useEffect(() => {
|
||||||
|
if (!product?.description) return;
|
||||||
|
|
||||||
|
const imageEl = imageColRef.current;
|
||||||
|
const infoEl = productInfoRef.current;
|
||||||
|
if (!imageEl || !infoEl) return;
|
||||||
|
|
||||||
|
const checkHeights = () => {
|
||||||
|
const descEl = descRef.current;
|
||||||
|
if (!descEl) return;
|
||||||
|
|
||||||
|
const descTrueH = descEl.scrollHeight;
|
||||||
|
const descVisibleH = descEl.getBoundingClientRect().height;
|
||||||
|
|
||||||
|
// ── Mobil: tek kolon layout, sabit eşik kullan ──────────────────
|
||||||
|
if (window.innerWidth <= 639) {
|
||||||
|
const MOBILE_THRESHOLD = 220;
|
||||||
|
if (descTrueH > MOBILE_THRESHOLD) {
|
||||||
|
setShowReadMore(true);
|
||||||
|
setCollapsedMaxHeight(MOBILE_THRESHOLD);
|
||||||
|
} else {
|
||||||
|
setShowReadMore(false);
|
||||||
|
setCollapsedMaxHeight(null);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Desktop/tablet: resim kolonu yüksekliğiyle karşılaştır ──────
|
||||||
|
const imageH = imageEl.getBoundingClientRect().height;
|
||||||
|
if (imageH === 0) return;
|
||||||
|
|
||||||
|
const infoCurrentH = infoEl.getBoundingClientRect().height;
|
||||||
|
// Info kolonunun gerçek (kısıtsız) yüksekliği:
|
||||||
|
const infoTrueH = infoCurrentH + (descTrueH - descVisibleH);
|
||||||
|
|
||||||
|
if (infoTrueH > imageH) {
|
||||||
|
const overflow = infoTrueH - imageH;
|
||||||
|
const newDescMaxH = Math.max(descTrueH - overflow, 60);
|
||||||
|
setShowReadMore(true);
|
||||||
|
setCollapsedMaxHeight(newDescMaxH);
|
||||||
|
} else {
|
||||||
|
setShowReadMore(false);
|
||||||
|
setCollapsedMaxHeight(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// İlk kontrol (DOM yerleştikten sonra)
|
||||||
|
const raf = requestAnimationFrame(checkHeights);
|
||||||
|
|
||||||
|
const ro = new ResizeObserver(checkHeights);
|
||||||
|
ro.observe(imageEl);
|
||||||
|
ro.observe(infoEl);
|
||||||
|
|
||||||
|
// Mobil↔desktop geçişi için window resize de dinlenir
|
||||||
|
window.addEventListener("resize", checkHeights);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelAnimationFrame(raf);
|
||||||
|
ro.disconnect();
|
||||||
|
window.removeEventListener("resize", checkHeights);
|
||||||
|
};
|
||||||
|
}, [product?.description]);
|
||||||
|
|
||||||
const { getCartItem } = useCart();
|
const { getCartItem } = useCart();
|
||||||
|
|
||||||
const [addToCart] = useAddToCartMutation();
|
const [addToCart] = useAddToCartMutation();
|
||||||
@@ -73,7 +157,7 @@ const ProductPage = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const qty = parseInt(
|
const qty = parseInt(
|
||||||
cartItem?.quantity || cartItem?.product_quantity || 0,
|
cartItem?.quantity || cartItem?.product_quantity || 0,
|
||||||
10
|
10,
|
||||||
);
|
);
|
||||||
setLocalQuantity(qty);
|
setLocalQuantity(qty);
|
||||||
setPendingQuantity(qty);
|
setPendingQuantity(qty);
|
||||||
@@ -83,7 +167,7 @@ const ProductPage = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (Array.isArray(favoriteProducts)) {
|
if (Array.isArray(favoriteProducts)) {
|
||||||
const isFav = favoriteProducts.some(
|
const isFav = favoriteProducts.some(
|
||||||
(fav) => fav.product?.id === product?.id
|
(fav) => fav.product?.id === product?.id,
|
||||||
);
|
);
|
||||||
setLocalIsFavorite(isFav);
|
setLocalIsFavorite(isFav);
|
||||||
}
|
}
|
||||||
@@ -180,10 +264,9 @@ const ProductPage = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const serverQty = parseInt(
|
const serverQty = parseInt(
|
||||||
cartItem?.quantity || cartItem?.product_quantity || 0,
|
cartItem?.quantity || cartItem?.product_quantity || 0,
|
||||||
10
|
10,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Sadece miktar değiştiyse ve 0'dan büyükse güncelle (0 ise Remove triggerlanır)
|
|
||||||
if (pendingQuantity === serverQty || pendingQuantity <= 0) {
|
if (pendingQuantity === serverQty || pendingQuantity <= 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -197,7 +280,6 @@ const ProductPage = ({
|
|||||||
}).unwrap();
|
}).unwrap();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to update cart item:", error);
|
console.error("Failed to update cart item:", error);
|
||||||
// Hata durumunda geri al
|
|
||||||
setLocalQuantity(serverQty);
|
setLocalQuantity(serverQty);
|
||||||
setPendingQuantity(serverQty);
|
setPendingQuantity(serverQty);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -225,12 +307,74 @@ const ProductPage = ({
|
|||||||
|
|
||||||
if (!product) return <div>Can not find product</div>;
|
if (!product) return <div>Can not find product</div>;
|
||||||
|
|
||||||
const imageUrl = product.media?.[0]?.thumbnail || "";
|
|
||||||
const categoryName = product.categories?.[0]?.name || "Category";
|
const categoryName = product.categories?.[0]?.name || "Category";
|
||||||
const categoryId = product.categories?.[0]?.id;
|
const categoryId = product.categories?.[0]?.id;
|
||||||
|
|
||||||
const handleCategoryClick = (categoryId) => {
|
const handleCategoryClick = (categoryId) => {
|
||||||
navigate(`/category/${categoryId}`);
|
navigate(`/category/${categoryId}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ── Cart + favorite butonları (desktop purchase card + mobile bar'da ortak) ──
|
||||||
|
const CartButtons = () => (
|
||||||
|
<div className={styles.Btn}>
|
||||||
|
{showFavoriteButton && (
|
||||||
|
<button
|
||||||
|
className={styles.favoriteButton}
|
||||||
|
onClick={handleToggleFavorite}
|
||||||
|
>
|
||||||
|
{localIsFavorite ? <IoMdHeart /> : <IoMdHeartEmpty />}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showAddToCart && (
|
||||||
|
<>
|
||||||
|
{localQuantity > 0 ? (
|
||||||
|
<div className={styles.quantityControls}>
|
||||||
|
<button
|
||||||
|
onClick={handleQuantityDecrease}
|
||||||
|
className={styles.quantityBtn}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 9 11"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M1.41422 6.86246C0.633166 6.08141 0.633165 4.81508 1.41421 4.03403L4.61487 0.833374C5.8748 -0.426555 8.02908 0.465776 8.02908 2.24759V8.6489C8.02908 10.4307 5.8748 11.323 4.61487 10.0631L1.41422 6.86246Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<span>{localQuantity}</span>
|
||||||
|
<button
|
||||||
|
onClick={handleQuantityIncrease}
|
||||||
|
className={styles.quantityBtn}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 9 11"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M6.64389 4.03427C7.42494 4.81532 7.42494 6.08165 6.64389 6.8627L3.44324 10.0634C2.18331 11.3233 0.0290222 10.431 0.0290226 8.64914V2.24783C0.0290226 0.466021 2.18331 -0.426312 3.44324 0.833617L6.64389 4.03427Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
className={styles.addToCartButton}
|
||||||
|
onClick={handleAddToCart}
|
||||||
|
>
|
||||||
|
<FaShoppingCart />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
{/* Breadcrumb */}
|
{/* Breadcrumb */}
|
||||||
@@ -242,9 +386,10 @@ const ProductPage = ({
|
|||||||
<span>{product?.name || "Product"}</span>
|
<span>{product?.name || "Product"}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Product Details */}
|
{/* ── 3 kolon ana section ── */}
|
||||||
<div className={styles.productSection}>
|
<div className={styles.productSection}>
|
||||||
<div className={styles.productImage}>
|
{/* KOLON 1: Resim */}
|
||||||
|
<div className={styles.productImage} ref={imageColRef}>
|
||||||
<ImageCarousel
|
<ImageCarousel
|
||||||
images={product.media}
|
images={product.media}
|
||||||
altText={product.name}
|
altText={product.name}
|
||||||
@@ -252,181 +397,165 @@ const ProductPage = ({
|
|||||||
isDetailView={true}
|
isDetailView={true}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.productInfo}>
|
|
||||||
<h1 className={styles.productTitle}>{product.name}</h1>
|
|
||||||
<p
|
|
||||||
className={styles.productDescription}
|
|
||||||
dangerouslySetInnerHTML={{ __html: product.description }}
|
|
||||||
></p>
|
|
||||||
|
|
||||||
|
{/* KOLON 2: İsim + Meta + Description */}
|
||||||
|
<div className={styles.productInfo} ref={productInfoRef}>
|
||||||
|
{/* Meta tablo */}
|
||||||
<div className={styles.productMeta}>
|
<div className={styles.productMeta}>
|
||||||
<div className={styles.metaItem}>
|
<h1 className={styles.productTitle}>{product.name}</h1>
|
||||||
|
|
||||||
|
{/* <div className={styles.metaItem}>
|
||||||
<span className={styles.metaLabel}>
|
<span className={styles.metaLabel}>
|
||||||
{t("product.productCode")}
|
{t("product.productCode")}
|
||||||
</span>
|
</span>
|
||||||
<span className={styles.metaValue}>{product.id}</span>
|
<span className={styles.metaValue}>{product.id}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{product.barcode && (
|
{product.barcode && (
|
||||||
<div className={styles.metaItem}>
|
<div className={styles.metaItem}>
|
||||||
<span className={styles.metaLabel}>{t("product.barCode")}</span>
|
<span className={styles.metaLabel}>{t("product.barCode")}</span>
|
||||||
<span className={styles.metaValue}>{product.barcode}</span>
|
<span className={styles.metaValue}>{product.barcode}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)} */}
|
||||||
|
|
||||||
{product.brand?.name && (
|
{product.brand?.name && (
|
||||||
<div className={styles.metaItem}>
|
<a
|
||||||
|
href={`/brands/${product.brand.id}`}
|
||||||
|
target="_blank"
|
||||||
|
className={styles.metaItem}
|
||||||
|
>
|
||||||
<span className={styles.metaLabel}>{t("order.brand")}</span>
|
<span className={styles.metaLabel}>{t("order.brand")}</span>
|
||||||
<span className={styles.metaValue}>{product.brand.name}</span>
|
<span className={styles.metaValue}>{product.brand.name}</span>
|
||||||
</div>
|
</a>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{product.channel?.[0]?.name && (
|
{product.channel?.[0]?.name && (
|
||||||
<div className={styles.metaItem}>
|
|
||||||
|
<Link to={`/channel/${product.channel[0].id}`} target="_blank" state={{ clearFilters: true }} className={styles.metaItem}>
|
||||||
<span className={styles.metaLabel}>{t("order.channel")}</span>
|
<span className={styles.metaLabel}>{t("order.channel")}</span>
|
||||||
<span className={styles.metaValue}>
|
<span className={styles.metaValue}>
|
||||||
{product.channel[0].name}
|
{product.channel[0].name}
|
||||||
</span>
|
</span>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
)}
|
||||||
|
|
||||||
|
{product.properties?.length > 0 && (
|
||||||
|
product.properties.map((prop, index) => (
|
||||||
|
<div key={`${prop.attribute_id}-${index}`} className={styles.metaItem}>
|
||||||
|
<span className={styles.metaLabel}>{prop.name}</span>
|
||||||
|
<span className={styles.metaValue}>{prop.value}</span>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Description card */}
|
||||||
|
{product.description && (
|
||||||
|
<div className={`${styles.descriptionCard} ${!isDescExpanded && showReadMore ? styles.descriptionCardCollapsed : ''}`}>
|
||||||
|
<div className={styles.descriptionHeader}>
|
||||||
|
<div className={styles.descriptionIcon}>
|
||||||
|
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<p className={styles.descriptionTitle}>
|
||||||
|
{t("product.description")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className={styles.productDescriptionWrapper}>
|
||||||
|
<div
|
||||||
|
ref={descRef}
|
||||||
|
className={`${styles.productDescription} ${
|
||||||
|
!isDescExpanded && showReadMore ? styles.productDescriptionCollapsed : ""
|
||||||
|
}`}
|
||||||
|
style={
|
||||||
|
!isDescExpanded && showReadMore && collapsedMaxHeight
|
||||||
|
? { maxHeight: `${collapsedMaxHeight}px` }
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
dangerouslySetInnerHTML={{ __html: product.description }}
|
||||||
|
/>
|
||||||
|
{showReadMore && !isDescExpanded && (
|
||||||
|
<button
|
||||||
|
className={styles.readMoreBtn}
|
||||||
|
onClick={() => setIsDescExpanded(true)}
|
||||||
|
>
|
||||||
|
{t("product.readMore")}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{showReadMore && isDescExpanded && (
|
||||||
|
<button
|
||||||
|
className={styles.readMoreBtn}
|
||||||
|
onClick={() => setIsDescExpanded(false)}
|
||||||
|
>
|
||||||
|
{t("product.readLess")}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.productActions}>
|
|
||||||
<div className={styles.priceContainer}>
|
|
||||||
<span className={styles.price}>{product.price_amount} m.</span>
|
|
||||||
|
|
||||||
|
{/* KOLON 3: Satın alma kartı (sadece desktop/tablet) */}
|
||||||
|
<div className={styles.purchaseCol}>
|
||||||
|
<div className={styles.purchaseCard}>
|
||||||
|
{/* Fiyat */}
|
||||||
|
<div className={styles.priceRow}>
|
||||||
|
<span className={styles.priceLabel}>{t("product.price")}:</span>
|
||||||
|
<div className={styles.priceRight}>
|
||||||
|
{isPriceZero(product.price_amount) ? (
|
||||||
|
<span style={{ display: "inline-flex", alignItems: "center", gap: 6, fontWeight: 600 }}>
|
||||||
|
<PendingPriceBadge />
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span className={styles.price}>{product.price_amount} m.</span>
|
||||||
{product.old_price_amount && (
|
{product.old_price_amount && (
|
||||||
<span className={styles.oldPrice}>
|
<span className={styles.oldPrice}>
|
||||||
{product.old_price_amount} m.
|
{product.old_price_amount} m.
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
<div className={styles.Btn}>
|
|
||||||
{showFavoriteButton && (
|
|
||||||
<button
|
|
||||||
className={styles.favoriteButton}
|
|
||||||
onClick={handleToggleFavorite}
|
|
||||||
>
|
|
||||||
{localIsFavorite ? <IoMdHeart /> : <IoMdHeartEmpty />}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{showAddToCart && (
|
|
||||||
<>
|
|
||||||
{localQuantity > 0 ? (
|
|
||||||
<div className={styles.quantityControls}>
|
|
||||||
<button
|
|
||||||
onClick={handleQuantityDecrease}
|
|
||||||
className={styles.quantityBtn}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
viewBox="0 0 9 11"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M1.41422 6.86246C0.633166 6.08141 0.633165 4.81508 1.41421 4.03403L4.61487 0.833374C5.8748 -0.426555 8.02908 0.465776 8.02908 2.24759V8.6489C8.02908 10.4307 5.8748 11.323 4.61487 10.0631L1.41422 6.86246Z"
|
|
||||||
fill="white"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<span>{localQuantity}</span>
|
|
||||||
<button
|
|
||||||
onClick={handleQuantityIncrease}
|
|
||||||
className={styles.quantityBtn}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
viewBox="0 0 9 11"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M6.64389 4.03427C7.42494 4.81532 7.42494 6.08165 6.64389 6.8627L3.44324 10.0634C2.18331 11.3233 0.0290222 10.431 0.0290226 8.64914V2.24783C0.0290226 0.466021 2.18331 -0.426312 3.44324 0.833617L6.64389 4.03427Z"
|
|
||||||
fill="white"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
className={styles.addToCartButton}
|
|
||||||
onClick={handleAddToCart}
|
|
||||||
>
|
|
||||||
<FaShoppingCart />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
className={styles.productActionsMobile}
|
{/* Butonlar */}
|
||||||
style={{ position: "sticky", bottom: "59px" }}
|
<CartButtons />
|
||||||
>
|
</div>
|
||||||
<div className={styles.priceContainer}>
|
</div>
|
||||||
{" "}
|
</div>
|
||||||
|
|
||||||
|
{/* ── Mobile sticky bar ── */}
|
||||||
|
<div className={styles.productActionsMobile}>
|
||||||
|
<div className={styles.mobilePriceContainer}>
|
||||||
|
{isPriceZero(product.price_amount) ? (
|
||||||
|
<span style={{ display: "inline-flex", alignItems: "center", gap: 6, fontWeight: 600 }}>
|
||||||
|
{t("cart.pendingPriceTitle")} <PendingPriceBadge />
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
<span className={styles.price}>{product.price_amount} m.</span>
|
<span className={styles.price}>{product.price_amount} m.</span>
|
||||||
|
{product.old_price_amount && (
|
||||||
<span className={styles.oldPrice}>
|
<span className={styles.oldPrice}>
|
||||||
{product.old_price_amount} m.
|
{product.old_price_amount} m.
|
||||||
</span>
|
</span>
|
||||||
</div>
|
|
||||||
<div className={styles.Btn}>
|
|
||||||
{showFavoriteButton && (
|
|
||||||
<button
|
|
||||||
className={styles.favoriteButton}
|
|
||||||
onClick={handleToggleFavorite}
|
|
||||||
>
|
|
||||||
{localIsFavorite ? <IoMdHeart /> : <IoMdHeartEmpty />}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{showAddToCart && (
|
|
||||||
<>
|
|
||||||
{localQuantity > 0 ? (
|
|
||||||
<div className={styles.quantityControls}>
|
|
||||||
<button
|
|
||||||
onClick={handleQuantityDecrease}
|
|
||||||
className={styles.quantityBtn}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
viewBox="0 0 9 11"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M1.41422 6.86246C0.633166 6.08141 0.633165 4.81508 1.41421 4.03403L4.61487 0.833374C5.8748 -0.426555 8.02908 0.465776 8.02908 2.24759V8.6489C8.02908 10.4307 5.8748 11.323 4.61487 10.0631L1.41422 6.86246Z"
|
|
||||||
fill="white"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<span>{localQuantity}</span>
|
|
||||||
<button
|
|
||||||
onClick={handleQuantityIncrease}
|
|
||||||
className={styles.quantityBtn}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
viewBox="0 0 9 11"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M6.64389 4.03427C7.42494 4.81532 7.42494 6.08165 6.64389 6.8627L3.44324 10.0634C2.18331 11.3233 0.0290222 10.431 0.0290226 8.64914V2.24783C0.0290226 0.466021 2.18331 -0.426312 3.44324 0.833617L6.64389 4.03427Z"
|
|
||||||
fill="white"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
className={styles.addToCartButton}
|
|
||||||
onClick={handleAddToCart}
|
|
||||||
>
|
|
||||||
<FaShoppingCart />
|
|
||||||
</button>
|
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<div className={styles.mobileBtnContainer}>
|
||||||
|
<CartButtons />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
{/* Reviews */}
|
||||||
<ReviewSection
|
<ReviewSection
|
||||||
productId={productId}
|
productId={productId}
|
||||||
existingReviews={product.reviews_resources}
|
existingReviews={product.reviews_resources}
|
||||||
@@ -452,6 +581,7 @@ const ProductPage = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Stock modal */}
|
||||||
<Modal
|
<Modal
|
||||||
title={t("common.warning")}
|
title={t("common.warning")}
|
||||||
open={stockErrorModalVisible}
|
open={stockErrorModalVisible}
|
||||||
@@ -480,3 +610,4 @@ const ProductPage = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default ProductPage;
|
export default ProductPage;
|
||||||
|
|
||||||
|
|||||||
143
src/pages/Stores/Stores.module.scss
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
.storesContainer {
|
||||||
|
padding: 1rem 4.4rem;
|
||||||
|
max-width: 1336px;
|
||||||
|
margin: 0 auto;
|
||||||
|
@media screen and (max-width: 1023px) {
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchWrapper {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 40px;
|
||||||
|
svg {
|
||||||
|
position: absolute;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
transform: translateX(35%);
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 46%;
|
||||||
|
height: 38px;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
padding-left: 40px;
|
||||||
|
outline: none;
|
||||||
|
@media screen and (max-width: 1023px) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.categorySection {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: #111827;
|
||||||
|
cursor: pointer;
|
||||||
|
&:hover {
|
||||||
|
color: #aaaaaa;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.storesGrid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(180px, 180px));
|
||||||
|
gap: 16px;
|
||||||
|
@media screen and (max-width: 1023px) {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
@media screen and (max-width: 900px) {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
@media screen and (max-width: 798px) {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.storeCard {
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
text-align: center;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
cursor: pointer;
|
||||||
|
@media screen and (max-width: 900px) {
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.imageWrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
position: relative;
|
||||||
|
aspect-ratio: 4/3;
|
||||||
|
|
||||||
|
img {
|
||||||
|
height: 100%;
|
||||||
|
object-fit:contain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
background: #f3f4f6;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.storeName {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #374151;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton {
|
||||||
|
background: linear-gradient(90deg, #f3f4f6 25%, #e5e7eb 50%, #f3f4f6 75%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: shimmer 1.5s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shimmer {
|
||||||
|
0% {
|
||||||
|
background-position: 200% 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: -200% 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.logoFallback {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 120px;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 4px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
202
src/pages/Stores/index.jsx
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import InfiniteScroll from "react-infinite-scroll-component";
|
||||||
|
import { useLazyGetChannelsQuery } from "../../app/api/channelsApi";
|
||||||
|
import styles from "./Stores.module.scss";
|
||||||
|
import { CiSearch } from "react-icons/ci";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { Logo } from "../../components/Icons";
|
||||||
|
import Loader from "../../components/Loader/index";
|
||||||
|
import { Result, Button } from "antd";
|
||||||
|
|
||||||
|
const StoresPage = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
// Stores data state
|
||||||
|
const [allStores, setAllStores] = useState([]);
|
||||||
|
const [visibleStores, setVisibleStores] = useState([]);
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const [hasMore, setHasMore] = useState(true);
|
||||||
|
const itemsPerPage = 24;
|
||||||
|
|
||||||
|
// Use lazy query to have more control over when to fetch
|
||||||
|
const [getChannels, { data: channelsData, isLoading, isFetching, error }] =
|
||||||
|
useLazyGetChannelsQuery();
|
||||||
|
|
||||||
|
// Initial fetch on component mount
|
||||||
|
useEffect(() => {
|
||||||
|
getChannels({ page: 1, perPage: itemsPerPage });
|
||||||
|
}, [getChannels]);
|
||||||
|
|
||||||
|
// Process stores data when it arrives
|
||||||
|
useEffect(() => {
|
||||||
|
if (channelsData) {
|
||||||
|
const stores = channelsData.data || [];
|
||||||
|
const pagination = channelsData.pagination || {};
|
||||||
|
|
||||||
|
console.log("Stores Data Received:", {
|
||||||
|
count: stores.length,
|
||||||
|
page,
|
||||||
|
pagination
|
||||||
|
});
|
||||||
|
|
||||||
|
setAllStores((prev) => {
|
||||||
|
const existingIds = new Set(prev.map((store) => store.id));
|
||||||
|
const newStores = stores.filter(
|
||||||
|
(store) => !existingIds.has(store.id)
|
||||||
|
);
|
||||||
|
return [...prev, ...newStores];
|
||||||
|
});
|
||||||
|
|
||||||
|
// More robust hasMore logic
|
||||||
|
const hasNext = pagination.next_page_url ||
|
||||||
|
(pagination.current_page && pagination.last_page && pagination.current_page < pagination.last_page) ||
|
||||||
|
(stores.length === itemsPerPage);
|
||||||
|
|
||||||
|
setHasMore(!!hasNext);
|
||||||
|
}
|
||||||
|
}, [channelsData, page, itemsPerPage]);
|
||||||
|
|
||||||
|
// Process stores for display whenever all stores or search term changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (allStores.length > 0) {
|
||||||
|
const filteredStores = searchTerm
|
||||||
|
? allStores.filter((store) =>
|
||||||
|
store.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
|
)
|
||||||
|
: allStores;
|
||||||
|
|
||||||
|
// Grouping logic (similar to Brands, but defaults to "Stores")
|
||||||
|
const groupedStores = filteredStores.reduce((acc, store) => {
|
||||||
|
const type = store.type || "Stores";
|
||||||
|
if (!acc[type]) {
|
||||||
|
acc[type] = [];
|
||||||
|
}
|
||||||
|
acc[type].push(store);
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
const displayGroups = Object.entries(groupedStores)
|
||||||
|
.map(([type, stores]) => ({
|
||||||
|
title: type === "Stores" ? t("navbar.stores") : type.charAt(0).toUpperCase() + type.slice(1),
|
||||||
|
stores,
|
||||||
|
}))
|
||||||
|
.filter((group) => group.stores.length > 0);
|
||||||
|
|
||||||
|
setVisibleStores(displayGroups);
|
||||||
|
if (searchTerm) {
|
||||||
|
setHasMore(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [allStores, searchTerm, t]);
|
||||||
|
|
||||||
|
const loadMoreStores = () => {
|
||||||
|
if (!searchTerm && !isFetching && hasMore && allStores.length > 0) {
|
||||||
|
const nextPage = page + 1;
|
||||||
|
getChannels({ page: nextPage, perPage: itemsPerPage });
|
||||||
|
setPage(nextPage);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearch = (e) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
setSearchTerm(value);
|
||||||
|
setPage(1);
|
||||||
|
setAllStores([]);
|
||||||
|
setHasMore(true);
|
||||||
|
getChannels({ page: 1, perPage: itemsPerPage, search: value });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleStoreClick = (storeId) => {
|
||||||
|
navigate(`/channel/${storeId}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoading && page === 1) return <Loader />;
|
||||||
|
if (error)
|
||||||
|
return (
|
||||||
|
<Result
|
||||||
|
status="500"
|
||||||
|
title="500"
|
||||||
|
subTitle={t("common.error_occurred") || "Näbelli ýalňyşlyk ýüze çykdy."}
|
||||||
|
extra={
|
||||||
|
<Button type="primary" onClick={() => navigate("/")}>
|
||||||
|
{t("common.back_to_home") || "Baş sahypa gidiň"}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.storesContainer}>
|
||||||
|
<div className={styles.searchWrapper}>
|
||||||
|
<CiSearch />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder={t("common.search")}
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={handleSearch}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<InfiniteScroll
|
||||||
|
dataLength={allStores.length}
|
||||||
|
next={loadMoreStores}
|
||||||
|
hasMore={hasMore && !searchTerm}
|
||||||
|
loader={<div style={{ textAlign: 'center', padding: '20px' }}><Loader /></div>}
|
||||||
|
>
|
||||||
|
{visibleStores.map((group, index) => (
|
||||||
|
<section key={index} className={styles.categorySection}>
|
||||||
|
<h2>{group.title}</h2>
|
||||||
|
|
||||||
|
<div className={styles.storesGrid}>
|
||||||
|
{group.stores.map((store) => (
|
||||||
|
<div
|
||||||
|
key={store.id}
|
||||||
|
className={styles.storeCard}
|
||||||
|
onClick={() => handleStoreClick(store.id)}
|
||||||
|
>
|
||||||
|
<div className={styles.imageWrapper}>
|
||||||
|
{store.media?.[0]?.thumbnail ||
|
||||||
|
store.media?.[0]?.images_800x800 ||
|
||||||
|
store.logo ? (
|
||||||
|
<img
|
||||||
|
src={
|
||||||
|
store.media?.[0]?.thumbnail ||
|
||||||
|
store.media?.[0]?.images_800x800 ||
|
||||||
|
store.logo
|
||||||
|
}
|
||||||
|
alt={store.name}
|
||||||
|
width={120}
|
||||||
|
height={120}
|
||||||
|
onError={(e) => {
|
||||||
|
e.target.style.display = "none";
|
||||||
|
e.target.nextSibling.style.display = "flex";
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className={styles.logoFallback}>
|
||||||
|
<Logo width={60} height={60} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className={styles.logoFallback}
|
||||||
|
style={{ display: "none" }}
|
||||||
|
>
|
||||||
|
<Logo width={60} height={60} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.divider}></div>
|
||||||
|
<h3 className={styles.storeName}>{store.name}</h3>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
))}
|
||||||
|
</InfiniteScroll>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StoresPage;
|
||||||
@@ -2,6 +2,9 @@ import React, { useState, useEffect } from "react";
|
|||||||
import InfiniteScroll from "react-infinite-scroll-component";
|
import InfiniteScroll from "react-infinite-scroll-component";
|
||||||
import CategorySection from "../../components/CategorySection/index";
|
import CategorySection from "../../components/CategorySection/index";
|
||||||
import Carousel from "../../components/Banner/index";
|
import Carousel from "../../components/Banner/index";
|
||||||
|
import CategoryCarousel from "../../components/CategoryCarousel/CategoryCarousel";
|
||||||
|
import HomeBrands from "../../components/HomeBrands/index";
|
||||||
|
import FlashSales from "../../components/FlashSales";
|
||||||
import styles from "./Home.module.scss";
|
import styles from "./Home.module.scss";
|
||||||
import { useGetCollectionsQuery } from "../../app/api/collectionsApi";
|
import { useGetCollectionsQuery } from "../../app/api/collectionsApi";
|
||||||
import PageLoader from "../../components/Loader/pageLoader";
|
import PageLoader from "../../components/Loader/pageLoader";
|
||||||
@@ -20,7 +23,6 @@ const Home = () => {
|
|||||||
const processCollections = async (collectionsData) => {
|
const processCollections = async (collectionsData) => {
|
||||||
if (!collectionsData || !collectionsData.data) return [];
|
if (!collectionsData || !collectionsData.data) return [];
|
||||||
|
|
||||||
// Cache the processed collections to prevent duplicate processing
|
|
||||||
const collectionsWithProducts = [];
|
const collectionsWithProducts = [];
|
||||||
|
|
||||||
for (const collection of collectionsData.data) {
|
for (const collection of collectionsData.data) {
|
||||||
@@ -44,8 +46,6 @@ const Home = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const checkIfCollectionHasProducts = async (collectionId) => {
|
const checkIfCollectionHasProducts = async (collectionId) => {
|
||||||
// This is a placeholder - your actual implementation would check if products exist
|
|
||||||
// For now, we just return true as in your original code
|
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -71,7 +71,6 @@ const Home = () => {
|
|||||||
setPage(page + 1);
|
setPage(page + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if we've loaded all collections
|
|
||||||
if (endIndex >= collections.length) {
|
if (endIndex >= collections.length) {
|
||||||
setHasMore(false);
|
setHasMore(false);
|
||||||
}
|
}
|
||||||
@@ -80,7 +79,6 @@ const Home = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// if (isLoading) return <PageLoader />;
|
|
||||||
if (error)
|
if (error)
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -100,6 +98,9 @@ const Home = () => {
|
|||||||
return (
|
return (
|
||||||
<div className={styles.home}>
|
<div className={styles.home}>
|
||||||
<Carousel />
|
<Carousel />
|
||||||
|
<CategoryCarousel />
|
||||||
|
<HomeBrands />
|
||||||
|
<FlashSales />
|
||||||
<div className={styles.sections}>
|
<div className={styles.sections}>
|
||||||
<InfiniteScroll
|
<InfiniteScroll
|
||||||
dataLength={visibleCollections.length}
|
dataLength={visibleCollections.length}
|
||||||
@@ -113,7 +114,7 @@ const Home = () => {
|
|||||||
<CategorySection
|
<CategorySection
|
||||||
key={collection.id}
|
key={collection.id}
|
||||||
collection={collection}
|
collection={collection}
|
||||||
preventEmptyRender={true} // Add a prop to prevent rendering empty collections
|
preventEmptyRender={true}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</InfiniteScroll>
|
</InfiniteScroll>
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ const ContactUs = lazy(() => import("./pages/ContactUs/index.jsx"));
|
|||||||
const DeliveryTerms = lazy(() => import("./pages/DeliveryTerms/index.jsx"));
|
const DeliveryTerms = lazy(() => import("./pages/DeliveryTerms/index.jsx"));
|
||||||
const AboutUs = lazy(() => import("./pages/AboutUs/index.jsx"));
|
const AboutUs = lazy(() => import("./pages/AboutUs/index.jsx"));
|
||||||
const PrivacyPolicy = lazy(() => import("./pages/PrivacyPolicy/index.jsx"));
|
const PrivacyPolicy = lazy(() => import("./pages/PrivacyPolicy/index.jsx"));
|
||||||
|
const AdminPage = lazy(() => import("./pages/CarconfiguratorAdmin/index.jsx"));
|
||||||
|
const StoresPage = lazy(() => import("./pages/Stores/index.jsx"));
|
||||||
|
|
||||||
export default function Router() {
|
export default function Router() {
|
||||||
const routes = useRoutes([
|
const routes = useRoutes([
|
||||||
@@ -33,12 +35,14 @@ export default function Router() {
|
|||||||
children: [
|
children: [
|
||||||
{ path: "/", element: <Home /> },
|
{ path: "/", element: <Home /> },
|
||||||
{ path: "/brands", element: <BrandsPage /> },
|
{ path: "/brands", element: <BrandsPage /> },
|
||||||
|
{ path: "/stores", element: <StoresPage /> },
|
||||||
{ path: "/brands/:brandId", element: <Category /> },
|
{ path: "/brands/:brandId", element: <Category /> },
|
||||||
{ path: "/cart", element: <CartPage /> },
|
{ path: "/cart", element: <CartPage /> },
|
||||||
{ path: "/wishlist", element: <WishList /> },
|
{ path: "/wishlist", element: <WishList /> },
|
||||||
{ path: "/category/:categoryId", element: <Category /> },
|
{ path: "/category/:categoryId", element: <Category /> },
|
||||||
{ path: "/search", element: <Category /> },
|
{ path: "/search", element: <Category /> },
|
||||||
{ path: "/collections/:collectionId", element: <Category /> },
|
{ path: "/collections/:collectionId", element: <Category /> },
|
||||||
|
{ path: "/channel/:channelId", element: <Category /> },
|
||||||
{ path: "/product/:productId", element: <ProductDetail /> },
|
{ path: "/product/:productId", element: <ProductDetail /> },
|
||||||
{ path: "/profile", element: <ProfileMenu /> },
|
{ path: "/profile", element: <ProfileMenu /> },
|
||||||
{ path: "/orders", element: <Orders /> },
|
{ path: "/orders", element: <Orders /> },
|
||||||
@@ -47,6 +51,7 @@ export default function Router() {
|
|||||||
{ path: "/delivery-and-payment", element: <DeliveryTerms /> },
|
{ path: "/delivery-and-payment", element: <DeliveryTerms /> },
|
||||||
{ path: "/about-us", element: <AboutUs /> },
|
{ path: "/about-us", element: <AboutUs /> },
|
||||||
{ path: "/privacy-policy", element: <PrivacyPolicy /> },
|
{ path: "/privacy-policy", element: <PrivacyPolicy /> },
|
||||||
|
{ path: "/carconfigurator-admin", element: <AdminPage /> },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -1,11 +1,39 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react'
|
||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [
|
||||||
|
react(),
|
||||||
|
{
|
||||||
|
name: "data-api",
|
||||||
|
configureServer(server) {
|
||||||
|
server.middlewares.use("/frontend-api/data", (req, res, next) => {
|
||||||
|
if (req.method === "POST") {
|
||||||
|
let body = "";
|
||||||
|
req.on("data", (chunk) => { body += chunk; });
|
||||||
|
req.on("end", () => {
|
||||||
|
fs.writeFile("public/data.json", body, (err) => {
|
||||||
|
if (err) { res.statusCode = 500; res.end(err.message); }
|
||||||
|
else { res.statusCode = 200; res.end(JSON.stringify({ ok: true })); }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (req.method === "GET") {
|
||||||
|
fs.readFile("public/data.json", "utf-8", (err, data) => {
|
||||||
|
if (err) { res.statusCode = 500; res.end(err.message); }
|
||||||
|
else { res.statusCode = 200; res.setHeader("Content-Type", "application/json"); res.end(data); }
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
server: {
|
server: {
|
||||||
host: true,
|
host: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|||||||