diff --git a/package-lock.json b/package-lock.json index 8879ae4..e351eff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,7 +41,10 @@ "zustand": "^5.0.12" }, "devDependencies": { - "@types/node": "^25.6.0" + "@types/node": "^25.6.0", + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.7", + "typescript": "^4.9.5" } }, "node_modules/@adobe/css-tools": { @@ -3040,6 +3043,16 @@ "node": ">= 8" } }, + "node_modules/@oxc-project/types": { + "version": "0.149.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.149.0.tgz", + "integrity": "sha512-Efcc+iF0j3Bf67YjEqIqWXbX5XddXoK/Mw4K1/JuXwRCZ8N16VR7iT23nlCc9XrveFVh/E5Rqs2StT0V8v9LdA==", + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, "node_modules/@pbe/react-yandex-maps": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/@pbe/react-yandex-maps/-/react-yandex-maps-1.2.5.tgz", @@ -3103,6 +3116,286 @@ } } }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.8.tgz", + "integrity": "sha512-tN5aztYkKCte4i5SIrrz5yK/HMjEuCqCSCJa418jOV8tZ1cBY3YF2otxB1ktPxzsLA1BeTqwapK0bfjxNvHJVw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.8.tgz", + "integrity": "sha512-dIYTWl9XprMUiQFoc55KUyk/oS8SKYH3zFl0LTR7RT0Xj4hgSVyuJcroH8JUu8RcpF8fTB6E0aOwCkZoYPcDSQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.8.tgz", + "integrity": "sha512-PCSDQGXD2IyTEFrcgPyBM8jJuGmrbCMuoIOXdbEGVemruKACXoLQJrb+A45Z0L5t1RQkdfJprAYPkikbh7dzdA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.8.tgz", + "integrity": "sha512-Uk7lRsGhPFHVX/sAUC6D5H9Ol30dFHd6iquokll2th3LpdJ3F5CzQB+7DHn0Ri2mG+U7k2zXiPHDrwZenXhwSA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.8.tgz", + "integrity": "sha512-DjszaTEVogPqA5bYzsEeqDCQxbcp2fexQwKcRspYji2yzR68fCf+e4fx6kBSRDwX5/brZaHw/hWS9+A/+/w9sQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.8.tgz", + "integrity": "sha512-zmwa7FTmdzB6aaEEuuls18H6Ap5JmJPSoPTuXixeJZV6tG40SyLkApQtz1g8ptZtiEKqj9OM0oNLPh1AgvE31Q==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.8.tgz", + "integrity": "sha512-KdYQDPHwJVnbFwdTGMgxsI9SqblBlz6STGM+w1We/d5B8OWWidYH0MwkU/uA1wM5fIpO2MkOVxXrNzzuZhw9ew==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.8.tgz", + "integrity": "sha512-jFJTifHnNPY+yzOoNZQfSIysrVyXzEQPhPnOUjmD1bcQGHH6s7c8cViKWar8YplQImE5N9JRqMCLrM2CdxOrZA==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.8.tgz", + "integrity": "sha512-FhiOziBDWPBjbcmRzfLyIJnaP7AVMFXT7YCXPjXxj7wKU3vx24RjrCNN/zjvVa+N2vVoHJwCoUBvsrN/DG3zIA==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.8.tgz", + "integrity": "sha512-WnHfADMzOV2Y55wlx1hzzQnar/wDt/VdvWSD99r18Mz9ylNieIGOkRx3UV21h7m/eJvjySYJkO26VvGNFkwsIQ==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.8.tgz", + "integrity": "sha512-H9tRr5ibfXFVLxbPOseVewewFpl28zcEdjRDt2FTUZU7odxP0gEv1ki4/kGmcGOh78oRwZuuQllGLZ9zTJp84g==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.8.tgz", + "integrity": "sha512-UefiqfM3D6IVNlZ8tSGs9+Ejjud2T+oxO0IHADU45Y+lyEjD2dVFyZHbkfX0LUb5Zugo/oIv1eCO/KVYhgYJYA==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.8.tgz", + "integrity": "sha512-637Ke4kWSy6rp9cxQ9gMOXlxPgIw/c1beASV4M//3+9I4uwBVOOl74G+e3zyU3u19U7RkRl/HuewixZ/Z6+Rjg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.8.tgz", + "integrity": "sha512-xWBkPOF1Q9k/Gv1nQXnVdLxKu74jXppuOM4Z3mnypVUJJJwLsMl7hNJGRAUJoG8A5MgOI1ACKM+wBFxSJzKy4A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.8.tgz", + "integrity": "sha512-uz2ZvfgXbxqNwijjjbxrnvALwpyODDcgc1T1N8N3rf/DXKQmaFwmB4LX4yyjggpwN2obdQLb2rgirX5ffCWYng==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "license": "MIT", + "peer": true + }, "node_modules/@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -4146,6 +4439,26 @@ "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", "license": "MIT" }, + "node_modules/@types/react": { + "version": "19.2.18", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", + "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.7.tgz", + "integrity": "sha512-I8bPpDLcHBv1qiIiXDCy71Rt8eQDKJP0sMSWJphDdAcdqiJ1sGpZamavoEIRZmYzjia9LuEb2HlYdDpmoENpvQ==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, "node_modules/@types/resolve": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", @@ -6785,6 +7098,13 @@ "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", "license": "MIT" }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -12265,9 +12585,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", "funding": [ { "type": "github", @@ -12949,9 +13269,9 @@ } }, "node_modules/postcss": { - "version": "8.5.12", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz", - "integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==", + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", "funding": [ { "type": "opencollective", @@ -12968,7 +13288,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.18", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -15250,6 +15570,40 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rolldown": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.8.tgz", + "integrity": "sha512-Z67nTmhZe7anqnM/EjI392w5i/ANUinjip7QYsOyN37oayduxt3ksdX0hf5OOamkAd53BiIHfbfSzfUmzKFQqQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@oxc-project/types": "=0.149.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.8", + "@rolldown/binding-android-arm64": "1.2.8", + "@rolldown/binding-darwin-arm64": "1.2.8", + "@rolldown/binding-darwin-x64": "1.2.8", + "@rolldown/binding-freebsd-x64": "1.2.8", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.8", + "@rolldown/binding-linux-arm64-gnu": "1.2.8", + "@rolldown/binding-linux-arm64-musl": "1.2.8", + "@rolldown/binding-linux-ppc64-gnu": "1.2.8", + "@rolldown/binding-linux-s390x-gnu": "1.2.8", + "@rolldown/binding-linux-x64-gnu": "1.2.8", + "@rolldown/binding-linux-x64-musl": "1.2.8", + "@rolldown/binding-openharmony-arm64": "1.2.8", + "@rolldown/binding-win32-arm64-msvc": "1.2.8", + "@rolldown/binding-win32-x64-msvc": "1.2.8" + } + }, "node_modules/rollup": { "version": "2.80.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.80.0.tgz", @@ -16786,9 +17140,9 @@ "license": "MIT" }, "node_modules/tinyglobby": { - "version": "0.2.16", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", - "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "license": "MIT", "dependencies": { "fdir": "^6.5.0", @@ -17094,6 +17448,19 @@ "is-typedarray": "^1.0.0" } }, + "node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, "node_modules/unbox-primitive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", @@ -17333,6 +17700,370 @@ "node": ">= 0.8" } }, + "node_modules/vite": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.2.tgz", + "integrity": "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.26", + "rolldown": "~1.2.4", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.4.0 || ^0.5.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "license": "MPL-2.0", + "peer": true, + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/vite/node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/void-elements": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", diff --git a/package.json b/package.json index 9434387..6cd8488 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,9 @@ ] }, "devDependencies": { - "@types/node": "^25.6.0" + "@types/node": "^25.6.0", + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.7", + "typescript": "^4.9.5" } } diff --git a/src/components/feed/FeedTopBar.jsx b/src/components/feed/FeedTopBar.jsx deleted file mode 100644 index 13eb3a3..0000000 --- a/src/components/feed/FeedTopBar.jsx +++ /dev/null @@ -1,120 +0,0 @@ -import { useState, useRef, useEffect } from "react"; -import { motion, AnimatePresence } from "framer-motion"; -import { useTranslation } from "react-i18next"; -import MenuIcon from "../icons/MenuIcon"; -import NahodkaLogo from "../NahodkaLogo"; - -/** - * TopBar ленты (Telegram-style): - * слева — круглая стеклянная кнопка-логотип (открывает фильтр типа), - * справа — squircle-кнопка-гамбургер (открывает SideBar). - */ -export default function FeedTopBar({ onMenuClick, typeFilter, onTypeFilterChange }) { - const { t } = useTranslation(); - const [open, setOpen] = useState(false); - const menuRef = useRef(null); - const btnRef = useRef(null); - - const options = [ - { key: "loss", label: t("feed.filterLoss") }, - { key: null, label: t("feed.filterAll") }, - { key: "found", label: t("feed.filterFound") }, - ]; - - useEffect(() => { - if (!open) return; - const handler = (e) => { - if ( - menuRef.current && !menuRef.current.contains(e.target) && - btnRef.current && !btnRef.current.contains(e.target) - ) { - setOpen(false); - } - }; - document.addEventListener("mousedown", handler); - return () => document.removeEventListener("mousedown", handler); - }, [open]); - - const handleSelect = (key) => { - onTypeFilterChange(key); - setOpen(false); - }; - - return ( -
-
- - - - - -
- - - {open && ( - setOpen(false)} - /> - )} - - - - {open && ( - - {options.map((opt) => ( - - ))} - - )} - -
- ); -} \ No newline at end of file diff --git a/src/pages/MainFeed.jsx b/src/pages/MainFeed.jsx deleted file mode 100644 index a63256d..0000000 --- a/src/pages/MainFeed.jsx +++ /dev/null @@ -1,484 +0,0 @@ -import { useState, useEffect, useCallback, useMemo, useRef } from "react"; -import { useNavigate, useLocation } from "react-router-dom"; -import "../styles/index.css"; -import { getImageUrl } from "../config/authConstants"; -import Sidebar from "../components/Sidebar"; -import BottomNav from "../components/nav/BottomNav"; -import RightSidebar from "../components/RightSidebar"; -import CreateUpdatePost from "./CreateUpdatePost"; -import ImageViewer from "../components/ImageViewer"; -import CityFilter from "../components/CityFilter"; -import { ProfileModal } from "../components/profile"; -import AuthModal from "../components/AuthModal"; -import RulesModal from "./RulesModal"; -import FeedTopBar from "../components/feed/FeedTopBar"; -import FeedSearchBar from "../components/feed/FeedSearchBar"; - -import FeedContent from "../components/feed/FeedContent"; -import MapView from "../components/MapView"; -import PostDetailModal from "../components/PostDetailModal"; -import { useAuthContext } from "../context/AuthContext"; -import { useCities } from "../hooks/useCities"; -import { useFeedPosts } from "../hooks/useFeedPosts"; -import { useMyPosts } from "../hooks/useMyPosts"; -import { useCityFeed } from "../hooks/useCityFeed"; -import { useInfiniteScroll } from "../hooks/useInfiniteScroll"; -import { useModals } from "../hooks/useModals"; -import { useFeedActions } from "../hooks/useFeedActions"; -import { useFavorites } from "../hooks/useFavorites"; -import { useTranslation } from 'react-i18next'; -import { useProfile } from "../hooks/useProfile"; - -/** - * MainFeed — корневой компонент приложения. - * Композиция хуков состояния + презентационных компонентов ленты. - */ -export default function MainFeed() { - const navigate = useNavigate(); - const location = useLocation(); - const { user, token, isAuthenticated, updateUser } = useAuthContext(); - const { fetchProfile } = useProfile(); - const { cities } = useCities(); - const { loadFavorites, loadFavoritePosts, error: favoritesError } = useFavorites(); - const { t } = useTranslation(); - const modals = useModals(); - - const [sidebarOpen, setSidebarOpen] = useState(false); - const [activeNav, setActiveNav] = useState("home"); - const [typeFilter, setTypeFilter] = useState(null); - const [selectedCities, setSelectedCities] = useState([]); - const [searchQuery, setSearchQuery] = useState(""); - const [liked, setLiked] = useState(new Set()); - const [likeCountAdj, setLikeCountAdj] = useState({}); - const [favoritePosts, setFavoritePosts] = useState([]); - const profileFetchedFor = useRef(null); - const [favoritesLoading, setFavoritesLoading] = useState(false); - - const feedRef = useRef(null); - // Состояние ленты перед переходом на карту — чтобы вернуть юзера на то же место - const feedStateRef = useRef({ scrollTop: 0, url: "/feed", activeNav: "home" }); - const restoreScrollRef = useRef(false); - - const isSearchView = new URLSearchParams(location.search).get("view") === "search"; - const isMapView = new URLSearchParams(location.search).get("view") === "map"; - // параметры фокуса карты на конкретном объявлении (?view=map&post=ID&lat=..&lng=..) - const mapFocusParams = new URLSearchParams(location.search); - const focusPostId = isMapView ? mapFocusParams.get("post") : null; - const focusLat = parseFloat(mapFocusParams.get("lat")); - const focusLng = parseFloat(mapFocusParams.get("lng")); - const hasActiveFilters = searchQuery.trim().length > 0 || selectedCities.length > 0; - const hasSearchParams = searchQuery.trim().length > 0 || selectedCities.length > 0; - const searchPrompt = isSearchView && !hasSearchParams; - - const feed = useFeedPosts(); - const myPosts = useMyPosts(user?.id); - const cityFeed = useCityFeed(selectedCities); - - const profileData = useMemo(() => { - if (!user) return {}; - const nameParts = (user.name || "").trim().split(/\s+/); - const firstName = user.firstName || nameParts[0] || ""; - const lastName = user.lastName || nameParts.slice(1).join(" ") || ""; - return { - login: user.login || "", - firstName, - lastName, - email: user.email || "", - phone: user.phone || "", - avatar: getImageUrl(user.avatar), - id: user.id, - }; - }, [user]); - - const refreshCurrentList = useCallback(() => { - if (selectedCities.length > 0) { - cityFeed.searchCities(selectedCities); - } else { - feed.loadFeed(); - } - }, [selectedCities, cityFeed, feed]); - - const actions = useFeedActions({ - refresh: refreshCurrentList, - loadMyPosts: myPosts.loadMyPosts, - onAuthRequired: () => modals.open("auth"), - isAuthenticated, - t, - setLiked, - setLikeCountAdj, - }); - - // Загружаем лайки пользователя при авторизации - useEffect(() => { - if (isAuthenticated && token) { - loadFavorites().then(favoriteIds => { - const s = new Set(favoriteIds); - actions.syncLiked(s); - setLiked(s); - }); - } else { - actions.syncLiked(new Set()); - setLiked(new Set()); - } - setLikeCountAdj({}); - }, [isAuthenticated, token, loadFavorites]); - - // Открываем модалку аутентификации если пришли с лендинга - useEffect(() => { - if (location.state?.openAuth) { - modals.open("auth"); - window.history.replaceState({}, document.title); - } - }, [location.state]); // eslint-disable-line react-hooks/exhaustive-deps - - // При загрузке приложения подтягиваем свежие данные профиля (аватар и т.д.) - // с бэка, чтобы нижняя навигация и профиль отражали изменения с других - // устройств, а не устаревший кэш контекста. Дергаем один раз на пользователя. - useEffect(() => { - if (isAuthenticated && user?.id && profileFetchedFor.current !== user.id) { - profileFetchedFor.current = user.id; - fetchProfile(user.id).then(result => { - if (result.success) updateUser(result.data); - }); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isAuthenticated, user?.id]); - - // При открытии профиля подтягиваем свежие данные (аватар/профиль) с бэка, - // чтобы изменения с других устройств отражались, а не брались из кэша контекста - useEffect(() => { - if (modals.isOpen("profile") && isAuthenticated && user?.id) { - fetchProfile(user.id).then(result => { - if (result.success) updateUser(result.data); - }); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [modals.isOpen("profile"), isAuthenticated, user?.id]); - - // Загрузка основной ленты (не в режиме поиска) - useEffect(() => { - if (!isSearchView && selectedCities.length === 0 && !searchQuery.trim()) { - feed.loadFeed(0, "", false); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isSearchView]); - - // Дебаунс поиска: только когда в search view и есть параметры - useEffect(() => { - if (!isSearchView) return; - if (!hasSearchParams) return; - const timer = setTimeout(() => { - feed.loadFeed(0, searchQuery, false); - }, 400); - return () => clearTimeout(timer); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [searchQuery, selectedCities, isSearchView]); - - // Когда переключаемся на «Мои объявления» — подгружаем с бэкенда - useEffect(() => { - if (activeNav === "myads" && isAuthenticated && user?.id) { - myPosts.loadMyPosts(); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [activeNav, isAuthenticated, user?.id]); - - // Загрузка избранных постов при переходе во вкладку «Избранное» - useEffect(() => { - if (activeNav !== "favorites") return; - if (!isAuthenticated || !token) return; - let cancelled = false; - setFavoritesLoading(true); - loadFavoritePosts().then(posts => { - if (cancelled) return; - setFavoritePosts(posts); - setFavoritesLoading(false); - }); - return () => { cancelled = true; }; - }, [activeNav, isAuthenticated, token, loadFavoritePosts]); - - const sentinelRef = useInfiniteScroll( - () => feed.loadFeed(feed.page + 1, searchQuery, true), - activeNav === "home" && selectedCities.length === 0 && feed.hasMore && !feed.loading && !feed.loadingMore - ); - - const handleImageClick = useCallback((images, index, ad) => { - if (images?.length) modals.open("viewer", { images, index, ad }); - }, [modals]); - - const handleCreatePost = useCallback(() => { - if (!isAuthenticated) modals.open("auth"); - else modals.open("create"); - }, [isAuthenticated, modals]); - - const handleEdit = useCallback((ad) => { - if (!isAuthenticated) modals.open("auth"); - else modals.open("edit", ad); - }, [isAuthenticated, modals]); - - const handleHomeClick = useCallback(() => { - setActiveNav("home"); - setSearchQuery(""); - setSelectedCities([]); - if (isSearchView || isMapView) navigate("/feed"); - feedRef.current?.scrollTo({ top: 0, behavior: "smooth" }); - }, [isSearchView, isMapView, navigate]); - - const handleSearchClick = useCallback(() => { - setActiveNav("home"); - setSearchQuery(""); - setSelectedCities([]); - if (!isSearchView) navigate("/feed?view=search"); - else if (isMapView) navigate("/feed?view=search"); - feedRef.current?.scrollTo({ top: 0, behavior: "smooth" }); - }, [isSearchView, isMapView, navigate]); - - // Карта с объявлениями - const saveFeedState = useCallback(() => { - feedStateRef.current = { - scrollTop: feedRef.current?.scrollTop ?? 0, - url: `${location.pathname}${location.search}`, - activeNav, - }; - restoreScrollRef.current = true; - }, [location.pathname, location.search, activeNav]); - - const handleMapClick = useCallback(() => { - saveFeedState(); - setActiveNav("map"); - if (!isMapView) navigate("/feed?view=map"); - }, [isMapView, navigate, saveFeedState]); - - const handleMapPostClick = useCallback((postId) => { - modals.open("postDetail", { postId }); - }, [modals]); - - // Возврат с карты на ленту — на то же место, где юзер был до открытия карты - const handleMapBack = useCallback(() => { - const { url, activeNav: prevNav } = feedStateRef.current; - setActiveNav(prevNav || "home"); - navigate(url || "/feed", { replace: true }); - }, [navigate]); - - // Восстанавливаем прокрутку ленты после возврата с карты - useEffect(() => { - if (isMapView || !restoreScrollRef.current) return; - const el = feedRef.current; - if (el) el.scrollTop = feedStateRef.current.scrollTop; - restoreScrollRef.current = false; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isMapView, feed.ads, cityFeed.cityAds, feed.loading, cityFeed.cityLoading]); - - // Клик по локации на карточке — карта с фокусом на этом объявлении - const handleLocationClick = useCallback((ad) => { - saveFeedState(); - const params = new URLSearchParams({ view: "map", post: ad.id }); - if (ad.latitude != null && ad.longitude != null) { - params.set("lat", ad.latitude); - params.set("lng", ad.longitude); - } - setActiveNav("map"); - navigate(`/feed?${params.toString()}`); - }, [navigate, saveFeedState]); - - const handleProfileClick = useCallback(() => { - if (!isAuthenticated) modals.open("auth"); - else modals.open("profile"); - }, [isAuthenticated, modals]); - - const filteredAds = useMemo(() => { - let result; - if (activeNav === "myads") { - result = myPosts.myAds; - } else if (activeNav === "favorites") { - result = favoritePosts; - } else if (selectedCities.length > 0) { - result = cityFeed.cityAds; - } else { - result = feed.ads; - } - - if (activeNav === "items") { - result = result.filter(ad => ["Вещь", "Другое"].includes(ad.category)); - } else if (activeNav === "pets") { - result = result.filter(ad => ad.category === "Питомец"); - } - - if (typeFilter) { - result = result.filter(ad => ad.type === typeFilter); - } - return result; - }, [activeNav, myPosts.myAds, favoritePosts, cityFeed.cityAds, feed.ads, selectedCities, typeFilter]); - - const listLoading = activeNav === "myads" - ? myPosts.loading - : activeNav === "favorites" - ? favoritesLoading - : feed.loading; - const listError = activeNav === "myads" - ? myPosts.error - : activeNav === "favorites" - ? favoritesError - : feed.error; - - return ( -
- setSidebarOpen(false)} - onNavClick={(key) => { - setActiveNav(key); - setSearchQuery(""); - setSelectedCities([]); - if (isMapView || isSearchView) navigate("/feed"); - }} - isAuthenticated={isAuthenticated} - onLoginClick={() => modals.open("auth")} - onRulesClick={() => modals.open("rules")} - onReviewsClick={() => navigate("/reviews")} - onCreatePost={handleCreatePost} - onProfileClick={() => modals.open("profile")} - profileData={profileData} - /> - -
- {isSearchView && ( - setSidebarOpen(v => !v)} - typeFilter={typeFilter} - onTypeFilterChange={setTypeFilter} - /> - )} -
- - - {activeNav === "home" && ( -
- -
- )} -
- -
- {isMapView ? ( - - ) : ( - <> -
- {!isSearchView && ( - setSidebarOpen(v => !v)} - typeFilter={typeFilter} - onTypeFilterChange={setTypeFilter} - /> - )} - 0} - error={listError} - filteredAds={filteredAds} - activeNav={activeNav} - searchPrompt={searchPrompt} - liked={liked} - likeCountAdj={likeCountAdj} - onLike={actions.toggleLike} - onEdit={handleEdit} - onDelete={actions.handleDelete} - onMarkFound={actions.handleMarkFound} - onImageClick={handleImageClick} - onLocationClick={handleLocationClick} - onAuthRequired={() => modals.open("auth")} - t={t} - /> - {activeNav === "home" && selectedCities.length === 0 && !feed.loading && ( -
- )} - {feed.loadingMore && ( -
-
-
- )} -
- modals.open("profile")} - onLoginClick={() => modals.open("auth")} - onNavClick={(key) => { - setActiveNav(key); - setSearchQuery(""); - setSelectedCities([]); - if (isMapView || isSearchView) navigate("/feed"); - }} - /> - - )} -
-
- - {modals.isOpen("create") && isAuthenticated && ( - - )} - - {modals.isOpen("edit") && isAuthenticated && ( - - )} - - {modals.isOpen("profile") && isAuthenticated && ( - - )} - - {modals.isOpen("auth") && } - - {modals.isOpen("rules") && } - - {modals.isOpen("viewer") && ( - - )} - - {modals.isOpen("postDetail") && ( - { - modals.open("viewer", { images, index, ad }); - }} - onLocationClick={(ad) => { - modals.close(); - handleLocationClick(ad); - }} - onAuthRequired={() => modals.open("auth")} - /> - )} - - -
- ); -} diff --git a/src/styles/base.css b/src/styles/base.css index 98d1602..b9b4d5d 100644 --- a/src/styles/base.css +++ b/src/styles/base.css @@ -13,6 +13,7 @@ --err: #ed4956; --bg-app: #fafafa; + --bg-rgb: 250, 250, 250; --bg-card: #ffffff; --bg-card-popup: #ffffff; --bg-surface: #ffffff; @@ -35,6 +36,7 @@ --err: #ed4956; --bg-app: #000000; + --bg-rgb: 0, 0, 0; --bg-card: #1b1b1b; --bg-card-popup: #262626; --bg-surface: #000000; diff --git a/src/styles/layout.css b/src/styles/layout.css index d681564..8d4c46c 100644 --- a/src/styles/layout.css +++ b/src/styles/layout.css @@ -1,3 +1,17 @@ +/* ───────────────────────────────────────────────────────────────────── + СТЕКЛЯННАЯ ШАПКА + ЗАТУХАНИЕ ЛЕНТЫ (Telegram Web style) + ───────────────────────────────────────────────────────────────────── + + Регистрация кастомного свойства: именно благодаря @property смена + --feed-fade-height анимируется transition'ом. В браузерах без поддержки + @property значение просто меняется мгновенно (graceful degradation). +*/ +@property --feed-fade-height { + syntax: ''; + inherits: false; + initial-value: 48px; +} + /* ── App Shell (Instagram-style) ── */ .f-app { display: flex; height: 100vh; height: 100dvh; @@ -19,17 +33,46 @@ } /* ── Topbar (telegram-style; mobile only; desktop uses the nav sidebar) ── - Фон отсутствует полностью: стеклянными выглядят только сами кнопки. */ + «Стекло»: полупрозрачный фон темы + мягкий blur по содержимому под шапкой. + blur/saturate явно умеренные — сильно агрессивное размытие даёт + пересвеченное «стекло» (blur 12px + saturate ~135%). */ .f-topbar { position: sticky; top: 0; - z-index: 100; + z-index: 10; display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 8px 12px; padding-top: calc(8px + env(safe-area-inset-top)); + /* Без backdrop-filter: blur имеет жёсткую границу по кромке прямоугольника, + которую маской не убрать. Плавность даёт только сквозной альфа-градиент, + сходящийся к 0 уже к низу бара. */ + background: linear-gradient( + to bottom, + rgba(var(--bg-rgb), 0.14) 0%, + rgba(var(--bg-rgb), 0.07) 55%, + rgba(var(--bg-rgb), 0.05) 100% + ); + transition: transform 0.25s ease, background-color 0.25s ease; +} + +/* Продолжение того же сквозного градиента ниже бара: цвет уходит в 0, + резкой границы нет, «плашки» не видно вообще. */ +.f-topbar::after { + content: ""; + position: absolute; + left: 0; + right: 0; + top: 100%; + height: 64px; + pointer-events: none; + background: linear-gradient( + to bottom, + rgba(var(--bg-rgb), 0.05) 0%, + rgba(var(--bg-rgb), 0) 100% + ); } /* Кнопка-логотип по центру: wordmark «Находка» в округлой стеклянной капсуле */ @@ -206,18 +249,22 @@ .f-topbar-dropdown-wrap { position: relative; } -/* В ленте шапка липнет к верху скроллера: контент прокручивается под ней - и «замыливается» через backdrop-filter на .f-topbar */ -.f-feed > .f-topbar-dropdown-wrap { - position: sticky; - top: 0; - z-index: 100; - margin: -20px -16px 0; +.f-topbar-dropdown-wrap .f-topbar.f-topbar-hidden { + /* Плавное скрытие шапки при скролле вниз (translateY вверх, за кромку .f-feed). + Висим на самом .f-topbar, а НЕ на обёртке: transform на обёртке создал бы + containing block для position:fixed оверлея дропдауна и сломал бы клик вне. */ + transform: translateY(-100%); } -@media (max-width: 900px) { - .f-feed > .f-topbar-dropdown-wrap { - margin: -10px -10px 0; - } +/* В ленте шапка лежит ПОВЕРХ маскированного скролл-контейнера (.f-feed-scroll): + по z-index она выше «слоя затухания», но ниже, чем сам контент не бывает + никогда — жёстко прижата к верху .f-feed. */ +.f-feed > .f-topbar-dropdown-wrap { + position: absolute; + top: 0; + left: 0; + right: 0; + z-index: 10; + margin: 0; } .f-topbar-dropdown-overlay { @@ -577,14 +624,45 @@ /* ── Feed Wrap + Scroll ── */ .f-wrap { display: flex; flex: 1; overflow: hidden; min-height: 0; } +/* Внешний контейнер ленты: не скроллится, лишь позиционирует слои. + Сверху — стеклянная шапка (абсолютный слой), под ней — маскированный + скролл-список; такой порядок даёт нужную иерархию z-index. */ .f-feed { - flex: 1; overflow-y: scroll; overflow-x: hidden; overscroll-behavior: contain; min-height: 0; - padding: 20px 16px; display: flex; flex-direction: column; gap: 16px; - width: 100%; max-width: 660px; margin: 0 auto; + position: relative; + overflow: hidden; + flex: 1; + min-height: 0; + width: 100%; + max-width: 660px; + margin: 0 auto; } -.f-feed::-webkit-scrollbar { width: 4px; } -.f-feed::-webkit-scrollbar-track { background: transparent; } -.f-feed::-webkit-scrollbar-thumb { background: var(--muted2); border-radius: 2px; } + +/* ── Слой затухания ленты ── + mask-image на самом контейнере списка (НЕ blur, НЕ opacity карточек): + верхние элементы, доезжающие до кромки под FeedTopBar, «растворяются». + Маска якорена к border-box скролл-контейнера, т.е. НЕ уезжает вместе + с карточками — зона затухания всегда под шапкой. Высота зоны задаётся + --feed-fade-height (управляется из React/ResizeObserver) и через + @property + transition анимируется при изменении. + padding-top синхронизирован с этой же переменной, чтобы первая карточка + в состоянии покоя не проваливалась под шапку. */ +.f-feed-scroll { + position: absolute; + inset: 0; + overflow-y: scroll; + overflow-x: hidden; + overscroll-behavior: contain; + display: flex; + flex-direction: column; + gap: 16px; + padding: calc(var(--feed-fade-height, 48px) + 20px) 16px 20px; + -webkit-mask-image: linear-gradient(to bottom, transparent 0, black var(--feed-fade-height, 48px)); + mask-image: linear-gradient(to bottom, transparent 0, black var(--feed-fade-height, 48px)); + transition: --feed-fade-height 0.2s ease; +} +.f-feed-scroll::-webkit-scrollbar { width: 4px; } +.f-feed-scroll::-webkit-scrollbar-track { background: transparent; } +.f-feed-scroll::-webkit-scrollbar-thumb { background: var(--muted2); border-radius: 2px; } @media (min-width: 900px) { .f-main-feed .f-topbar { @@ -611,8 +689,8 @@ bottom: calc(84px + env(safe-area-inset-bottom)); } - .f-feed { - padding: 10px; + .f-feed-scroll { + padding: calc(var(--feed-fade-height, 48px) + 10px) 10px calc(150px + env(safe-area-inset-bottom)); gap: 12px; } @@ -645,11 +723,6 @@ margin-top: 4px; border-radius: 999px; } .f-topbar-dropdown-item { padding: 7px 13px; font-size: 0.8rem; } - - /* Нижняя панель fixed — освобождаем место в конце ленты */ - .f-feed { - padding-bottom: calc(150px + env(safe-area-inset-bottom)); - } } @media (max-width: 600px) {