diff --git a/build.zip b/build.zip
new file mode 100644
index 0000000..9c0baa8
Binary files /dev/null and b/build.zip differ
diff --git a/eslint.config.js b/eslint.config.js
index ea36dd3..4fa125d 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -14,8 +14,16 @@ export default defineConfig([
reactRefresh.configs.vite,
],
languageOptions: {
+ ecmaVersion: 2020,
globals: globals.browser,
- parserOptions: { ecmaFeatures: { jsx: true } },
+ parserOptions: {
+ ecmaVersion: 'latest',
+ ecmaFeatures: { jsx: true },
+ sourceType: 'module',
+ },
+ },
+ rules: {
+ 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
},
},
])
diff --git a/package-lock.json b/package-lock.json
index 2cd3f12..0d0e3f0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,19 +8,23 @@
"name": "vkminiapp",
"version": "0.0.0",
"dependencies": {
+ "@react-oauth/google": "^0.13.5",
"@vkontakte/vk-bridge": "^3.0.2",
"axios": "^1.16.1",
"framer-motion": "^12.40.0",
"lucide-react": "^1.16.0",
"react": "^19.2.6",
"react-dom": "^19.2.6",
- "react-router-dom": "^7.15.1"
+ "react-router-dom": "^7.15.1",
+ "styled-components": "^6.4.2"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
+ "@tailwindcss/postcss": "^4.3.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1",
+ "@vkontakte/vk-miniapps-deploy": "^1.0.2",
"autoprefixer": "^10.5.0",
"eslint": "^10.3.0",
"eslint-plugin-react-hooks": "^7.1.1",
@@ -31,6 +35,19 @@
"vite": "^8.0.12"
}
},
+ "node_modules/@alloc/quick-lru": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
+ "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/@babel/code-frame": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
@@ -305,6 +322,21 @@
"tslib": "^2.4.0"
}
},
+ "node_modules/@emotion/is-prop-valid": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz",
+ "integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==",
+ "license": "MIT",
+ "dependencies": {
+ "@emotion/memoize": "^0.9.0"
+ }
+ },
+ "node_modules/@emotion/memoize": {
+ "version": "0.9.0",
+ "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz",
+ "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==",
+ "license": "MIT"
+ },
"node_modules/@eslint-community/eslint-utils": {
"version": "4.9.1",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
@@ -578,6 +610,16 @@
"url": "https://github.com/sponsors/Boshen"
}
},
+ "node_modules/@react-oauth/google": {
+ "version": "0.13.5",
+ "resolved": "https://registry.npmjs.org/@react-oauth/google/-/google-0.13.5.tgz",
+ "integrity": "sha512-xQWri2s/3nNekZJ4uuov2aAfQYu83bN3864KcFqw2pK1nNbFurQIjPFDXhWaKH3IjYJ2r/9yyIIpsn5lMqrheQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0"
+ }
+ },
"node_modules/@rolldown/binding-android-arm64": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.2.tgz",
@@ -869,6 +911,289 @@
"tslib": "^2.8.0"
}
},
+ "node_modules/@tailwindcss/node": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.0.tgz",
+ "integrity": "sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/remapping": "^2.3.5",
+ "enhanced-resolve": "^5.21.0",
+ "jiti": "^2.6.1",
+ "lightningcss": "1.32.0",
+ "magic-string": "^0.30.21",
+ "source-map-js": "^1.2.1",
+ "tailwindcss": "4.3.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.0.tgz",
+ "integrity": "sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 20"
+ },
+ "optionalDependencies": {
+ "@tailwindcss/oxide-android-arm64": "4.3.0",
+ "@tailwindcss/oxide-darwin-arm64": "4.3.0",
+ "@tailwindcss/oxide-darwin-x64": "4.3.0",
+ "@tailwindcss/oxide-freebsd-x64": "4.3.0",
+ "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.0",
+ "@tailwindcss/oxide-linux-arm64-gnu": "4.3.0",
+ "@tailwindcss/oxide-linux-arm64-musl": "4.3.0",
+ "@tailwindcss/oxide-linux-x64-gnu": "4.3.0",
+ "@tailwindcss/oxide-linux-x64-musl": "4.3.0",
+ "@tailwindcss/oxide-wasm32-wasi": "4.3.0",
+ "@tailwindcss/oxide-win32-arm64-msvc": "4.3.0",
+ "@tailwindcss/oxide-win32-x64-msvc": "4.3.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-android-arm64": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.0.tgz",
+ "integrity": "sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-darwin-arm64": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.0.tgz",
+ "integrity": "sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-darwin-x64": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.0.tgz",
+ "integrity": "sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-freebsd-x64": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.0.tgz",
+ "integrity": "sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.0.tgz",
+ "integrity": "sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.0.tgz",
+ "integrity": "sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm64-musl": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.0.tgz",
+ "integrity": "sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-x64-gnu": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.0.tgz",
+ "integrity": "sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-x64-musl": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.0.tgz",
+ "integrity": "sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.0.tgz",
+ "integrity": "sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==",
+ "bundleDependencies": [
+ "@napi-rs/wasm-runtime",
+ "@emnapi/core",
+ "@emnapi/runtime",
+ "@tybys/wasm-util",
+ "@emnapi/wasi-threads",
+ "tslib"
+ ],
+ "cpu": [
+ "wasm32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "^1.10.0",
+ "@emnapi/runtime": "^1.10.0",
+ "@emnapi/wasi-threads": "^1.2.1",
+ "@napi-rs/wasm-runtime": "^1.1.4",
+ "@tybys/wasm-util": "^0.10.1",
+ "tslib": "^2.8.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.0.tgz",
+ "integrity": "sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-win32-x64-msvc": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.0.tgz",
+ "integrity": "sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/postcss": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.3.0.tgz",
+ "integrity": "sha512-Jm05Tjx+9yCLGv5qw1c+84Psds8MnyrEQYCB+FFk2lgGiUjlRqdxke4mVTuYrj2xnVZqKim2Apr5ySuQRYAw/w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@alloc/quick-lru": "^5.2.0",
+ "@tailwindcss/node": "4.3.0",
+ "@tailwindcss/oxide": "4.3.0",
+ "postcss": "^8.5.10",
+ "tailwindcss": "4.3.0"
+ }
+ },
"node_modules/@tybys/wasm-util": {
"version": "0.10.2",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
@@ -956,6 +1281,73 @@
"@swc/helpers": "^0.5.21"
}
},
+ "node_modules/@vkontakte/vk-miniapps-deploy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@vkontakte/vk-miniapps-deploy/-/vk-miniapps-deploy-1.0.2.tgz",
+ "integrity": "sha512-IIBOCoKj+sFkXyZiwUwE0yNd5p/vpCdTDe6K80xhe9tNEcctugiTqyPE8WOzHx9yVfyFbYnF9z3oTpuMtfDtTg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "dependencies": {
+ "async": "^3.2.0",
+ "chalk": "^3.0.0",
+ "configstore": "^5.0.0",
+ "form-data": "^3.0.0",
+ "fs-extra": "^8.0.1",
+ "https-proxy-agent": "^7.0.6",
+ "node-fetch": "^2.6.0",
+ "prompts": "^2.1.0",
+ "require-module": "^0.1.0",
+ "zip-a-folder": "0.0.12"
+ },
+ "bin": {
+ "vk-miniapps-deploy": "bin/vk-miniapps-deploy"
+ },
+ "engines": {
+ "node": ">=8.10"
+ }
+ },
+ "node_modules/@vkontakte/vk-miniapps-deploy/node_modules/agent-base": {
+ "version": "7.1.4",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
+ "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/@vkontakte/vk-miniapps-deploy/node_modules/form-data": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.4.tgz",
+ "integrity": "sha512-f0cRzm6dkyVYV3nPoooP8XlccPQukegwhAnpoLcXy+X+A8KfpGOoXwDr9FLZd3wzgLaBGQBE3lY93Zm/i1JvIQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.2",
+ "mime-types": "^2.1.35"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/@vkontakte/vk-miniapps-deploy/node_modules/https-proxy-agent": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+ "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.2",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
"node_modules/acorn": {
"version": "8.16.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
@@ -1008,6 +1400,113 @@
"url": "https://github.com/sponsors/epoberezkin"
}
},
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/archiver": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/archiver/-/archiver-3.1.1.tgz",
+ "integrity": "sha512-5Hxxcig7gw5Jod/8Gq0OneVgLYET+oNHcxgWItq4TbhOzRLKNAFUb9edAftiMKXvXfCB0vbGrJdZDNq0dWMsxg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "archiver-utils": "^2.1.0",
+ "async": "^2.6.3",
+ "buffer-crc32": "^0.2.1",
+ "glob": "^7.1.4",
+ "readable-stream": "^3.4.0",
+ "tar-stream": "^2.1.0",
+ "zip-stream": "^2.1.2"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/archiver-utils": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz",
+ "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "glob": "^7.1.4",
+ "graceful-fs": "^4.2.0",
+ "lazystream": "^1.0.0",
+ "lodash.defaults": "^4.2.0",
+ "lodash.difference": "^4.5.0",
+ "lodash.flatten": "^4.4.0",
+ "lodash.isplainobject": "^4.0.6",
+ "lodash.union": "^4.6.0",
+ "normalize-path": "^3.0.0",
+ "readable-stream": "^2.0.0"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/archiver-utils/node_modules/readable-stream": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
+ "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "node_modules/archiver-utils/node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/archiver-utils/node_modules/string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
+ "node_modules/archiver/node_modules/async": {
+ "version": "2.6.4",
+ "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
+ "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "lodash": "^4.17.14"
+ }
+ },
+ "node_modules/async": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
+ "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@@ -1073,6 +1572,27 @@
"node": "18 || 20 || >=22"
}
},
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
"node_modules/baseline-browser-mapping": {
"version": "2.10.32",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.32.tgz",
@@ -1086,6 +1606,18 @@
"node": ">=6.0.0"
}
},
+ "node_modules/bl": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
+ "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "buffer": "^5.5.0",
+ "inherits": "^2.0.4",
+ "readable-stream": "^3.4.0"
+ }
+ },
"node_modules/brace-expansion": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
@@ -1133,6 +1665,41 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
+ "node_modules/buffer": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
+ "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.1.13"
+ }
+ },
+ "node_modules/buffer-crc32": {
+ "version": "0.2.13",
+ "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
+ "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
@@ -1146,6 +1713,15 @@
"node": ">= 0.4"
}
},
+ "node_modules/camelize": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz",
+ "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/caniuse-lite": {
"version": "1.0.30001793",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz",
@@ -1167,6 +1743,40 @@
],
"license": "CC-BY-4.0"
},
+ "node_modules/chalk": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
+ "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@@ -1179,6 +1789,80 @@
"node": ">= 0.8"
}
},
+ "node_modules/compress-commons": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-2.1.1.tgz",
+ "integrity": "sha512-eVw6n7CnEMFzc3duyFVrQEuY1BlHR3rYsSztyG32ibGMW722i3C6IizEGMFmfMU+A+fALvBIwxN3czffTcdA+Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "buffer-crc32": "^0.2.13",
+ "crc32-stream": "^3.0.1",
+ "normalize-path": "^3.0.0",
+ "readable-stream": "^2.3.6"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/compress-commons/node_modules/readable-stream": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
+ "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "node_modules/compress-commons/node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/compress-commons/node_modules/string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/configstore": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz",
+ "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "dot-prop": "^5.2.0",
+ "graceful-fs": "^4.1.2",
+ "make-dir": "^3.0.0",
+ "unique-string": "^2.0.0",
+ "write-file-atomic": "^3.0.0",
+ "xdg-basedir": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/convert-source-map": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
@@ -1199,6 +1883,37 @@
"url": "https://opencollective.com/express"
}
},
+ "node_modules/core-util-is": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
+ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/crc": {
+ "version": "3.8.0",
+ "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz",
+ "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "buffer": "^5.1.0"
+ }
+ },
+ "node_modules/crc32-stream": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-3.0.1.tgz",
+ "integrity": "sha512-mctvpXlbzsvK+6z8kJwSJ5crm7yBwrQMTybJzMw1O4lLGJqjlDCXY2Zw7KheiA6XBEcBmfLx1D88mjRGVJtY9w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "crc": "^3.4.4",
+ "readable-stream": "^3.4.0"
+ },
+ "engines": {
+ "node": ">= 6.9.0"
+ }
+ },
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -1214,11 +1929,40 @@
"node": ">= 8"
}
},
+ "node_modules/crypto-random-string": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz",
+ "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/css-color-keywords": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz",
+ "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/css-to-react-native": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz",
+ "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==",
+ "license": "MIT",
+ "dependencies": {
+ "camelize": "^1.0.0",
+ "css-color-keywords": "^1.0.0",
+ "postcss-value-parser": "^4.0.2"
+ }
+ },
"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==",
- "dev": true,
"license": "MIT"
},
"node_modules/debug": {
@@ -1264,6 +2008,19 @@
"node": ">=8"
}
},
+ "node_modules/dot-prop": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz",
+ "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-obj": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -1285,6 +2042,30 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/end-of-stream": {
+ "version": "1.4.5",
+ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
+ "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "once": "^1.4.0"
+ }
+ },
+ "node_modules/enhanced-resolve": {
+ "version": "5.22.0",
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.22.0.tgz",
+ "integrity": "sha512-xYcDWrpELkFzz9SpZ3PlI6Eu6eD93Yf0WLDRxikGhWJ3MAir2SNZTIVCVZqZ/NUyx8AdMc2gT9C0gPiw18kG+A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.4",
+ "tapable": "^2.3.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
@@ -1702,6 +2483,35 @@
}
}
},
+ "node_modules/fs-constants": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
+ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fs-extra": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
+ "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^4.0.0",
+ "universalify": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=6 <7 || >=8"
+ }
+ },
+ "node_modules/fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+ "dev": true,
+ "license": "ISC"
+ },
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -1773,6 +2583,28 @@
"node": ">= 0.4"
}
},
+ "node_modules/glob": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
"node_modules/glob-parent": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
@@ -1786,6 +2618,37 @@
"node": ">=10.13.0"
}
},
+ "node_modules/glob/node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/glob/node_modules/brace-expansion": {
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz",
+ "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/glob/node_modules/minimatch": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/globals": {
"version": "17.6.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-17.6.0.tgz",
@@ -1811,6 +2674,23 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
@@ -1880,6 +2760,27 @@
"node": ">= 6"
}
},
+ "node_modules/ieee754": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -1900,6 +2801,25 @@
"node": ">=0.8.19"
}
},
+ "node_modules/inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+ "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "dev": true,
+ "license": "ISC"
+ },
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -1923,6 +2843,30 @@
"node": ">=0.10.0"
}
},
+ "node_modules/is-obj": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz",
+ "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-typedarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
+ "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/isarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+ "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -1930,6 +2874,16 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/jiti": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz",
+ "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jiti": "lib/jiti-cli.mjs"
+ }
+ },
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -1984,6 +2938,16 @@
"node": ">=6"
}
},
+ "node_modules/jsonfile": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
+ "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
+ "dev": true,
+ "license": "MIT",
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -1994,6 +2958,62 @@
"json-buffer": "3.0.1"
}
},
+ "node_modules/kleur": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
+ "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/lazystream": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz",
+ "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "readable-stream": "^2.0.5"
+ },
+ "engines": {
+ "node": ">= 0.6.3"
+ }
+ },
+ "node_modules/lazystream/node_modules/readable-stream": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
+ "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "node_modules/lazystream/node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lazystream/node_modules/string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
"node_modules/levn": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
@@ -2297,6 +3317,48 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/lodash": {
+ "version": "4.18.1",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
+ "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lodash.defaults": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
+ "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lodash.difference": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz",
+ "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lodash.flatten": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
+ "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lodash.isplainobject": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
+ "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lodash.union": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz",
+ "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
@@ -2316,6 +3378,32 @@
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/make-dir": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
+ "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -2409,6 +3497,27 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/node-fetch": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
+ "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-url": "^5.0.0"
+ },
+ "engines": {
+ "node": "4.x || >=6.0.0"
+ },
+ "peerDependencies": {
+ "encoding": "^0.1.0"
+ },
+ "peerDependenciesMeta": {
+ "encoding": {
+ "optional": true
+ }
+ }
+ },
"node_modules/node-releases": {
"version": "2.0.46",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.46.tgz",
@@ -2419,6 +3528,26 @@
"node": ">=18"
}
},
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "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==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@@ -2479,6 +3608,16 @@
"node": ">=8"
}
},
+ "node_modules/path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
@@ -2542,7 +3681,6 @@
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
- "dev": true,
"license": "MIT"
},
"node_modules/prelude-ls": {
@@ -2555,6 +3693,27 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/process-nextick-args": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
+ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/prompts": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
+ "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "kleur": "^3.0.3",
+ "sisteransi": "^1.0.5"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/proxy-from-env": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
@@ -2633,6 +3792,38 @@
"react-dom": ">=18"
}
},
+ "node_modules/readable-stream": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/require-module": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/require-module/-/require-module-0.1.0.tgz",
+ "integrity": "sha512-fbr7gXnwot8k98dOUIq9KA4tvEot+CNMg1GR6j1v+7gI3aECMeyxmw2Ux0RWecPR6GfLqktVJ84GlTXoFlS2Cw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "resolve": "~0.6.1"
+ }
+ },
+ "node_modules/resolve": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-0.6.3.tgz",
+ "integrity": "sha512-UHBY3viPlJKf85YijDUcikKX6tmF4SokIDp518ZDVT92JNDcG5uKIthaT/owt3Sar0lwtOafsQuwrg22/v2Dwg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/rolldown": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.2.tgz",
@@ -2667,6 +3858,27 @@
"@rolldown/binding-win32-x64-msvc": "1.0.2"
}
},
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
"node_modules/scheduler": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
@@ -2712,6 +3924,20 @@
"node": ">=8"
}
},
+ "node_modules/signal-exit": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/sisteransi": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
+ "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -2722,6 +3948,71 @@
"node": ">=0.10.0"
}
},
+ "node_modules/string_decoder": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.2.0"
+ }
+ },
+ "node_modules/styled-components": {
+ "version": "6.4.2",
+ "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.4.2.tgz",
+ "integrity": "sha512-xZBhBJsMtGqb+aKcwKgaT+BtuFums9VynX2JRvXJGTx5UfZzN12rk5r4nVdhXYvRw+hE7yiYxVrOqJZaK2+Txg==",
+ "license": "MIT",
+ "dependencies": {
+ "@emotion/is-prop-valid": "1.4.0",
+ "css-to-react-native": "3.2.0",
+ "csstype": "3.2.3",
+ "stylis": "4.3.6"
+ },
+ "engines": {
+ "node": ">= 16"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/styled-components"
+ },
+ "peerDependencies": {
+ "css-to-react-native": ">= 3.2.0",
+ "react": ">= 16.8.0",
+ "react-dom": ">= 16.8.0",
+ "react-native": ">= 0.68.0"
+ },
+ "peerDependenciesMeta": {
+ "css-to-react-native": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ },
+ "react-native": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/stylis": {
+ "version": "4.3.6",
+ "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz",
+ "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==",
+ "license": "MIT"
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/tailwindcss": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.0.tgz",
@@ -2729,6 +4020,37 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/tapable": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz",
+ "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ }
+ },
+ "node_modules/tar-stream": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
+ "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "bl": "^4.0.3",
+ "end-of-stream": "^1.4.1",
+ "fs-constants": "^1.0.0",
+ "inherits": "^2.0.3",
+ "readable-stream": "^3.1.1"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/tinyglobby": {
"version": "0.2.16",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
@@ -2746,6 +4068,13 @@
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
+ "node_modules/tr46": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
@@ -2765,6 +4094,39 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/typedarray-to-buffer": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
+ "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-typedarray": "^1.0.0"
+ }
+ },
+ "node_modules/unique-string": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz",
+ "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "crypto-random-string": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/universalify": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
+ "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4.0.0"
+ }
+ },
"node_modules/update-browserslist-db": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
@@ -2806,6 +4168,13 @@
"punycode": "^2.1.0"
}
},
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/vite": {
"version": "8.0.14",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.14.tgz",
@@ -2884,6 +4253,24 @@
}
}
},
+ "node_modules/webidl-conversions": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
+ "dev": true,
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/whatwg-url": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+ "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tr46": "~0.0.3",
+ "webidl-conversions": "^3.0.0"
+ }
+ },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -2910,6 +4297,36 @@
"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==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/write-file-atomic": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz",
+ "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "imurmurhash": "^0.1.4",
+ "is-typedarray": "^1.0.0",
+ "signal-exit": "^3.0.2",
+ "typedarray-to-buffer": "^3.1.5"
+ }
+ },
+ "node_modules/xdg-basedir": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz",
+ "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
@@ -2930,6 +4347,31 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/zip-a-folder": {
+ "version": "0.0.12",
+ "resolved": "https://registry.npmjs.org/zip-a-folder/-/zip-a-folder-0.0.12.tgz",
+ "integrity": "sha512-wZGiWgp3z2TocBlzx3S5tsLgPbT39qG2uIZmn2MhYLVjhKIr2nMhg7i4iPDL4W3XvMDaOEEVU5ZB0Y/Pt6BLvA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "archiver": "^3.1.1"
+ }
+ },
+ "node_modules/zip-stream": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-2.1.3.tgz",
+ "integrity": "sha512-EkXc2JGcKhO5N5aZ7TmuNo45budRaFGHOmz24wtJR7znbNqDPmdZtUauKX6et8KAVseAMBOyWJqEpXcHTBsh7Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "archiver-utils": "^2.1.0",
+ "compress-commons": "^2.1.1",
+ "readable-stream": "^3.4.0"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/zod": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
diff --git a/package.json b/package.json
index 00dfaeb..351dd04 100644
--- a/package.json
+++ b/package.json
@@ -7,22 +7,27 @@
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
- "preview": "vite preview"
+ "preview": "vite preview",
+ "deploy": "npm run build && vk-miniapps-deploy"
},
"dependencies": {
+ "@react-oauth/google": "^0.13.5",
"@vkontakte/vk-bridge": "^3.0.2",
"axios": "^1.16.1",
"framer-motion": "^12.40.0",
"lucide-react": "^1.16.0",
"react": "^19.2.6",
"react-dom": "^19.2.6",
- "react-router-dom": "^7.15.1"
+ "react-router-dom": "^7.15.1",
+ "styled-components": "^6.4.2"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
+ "@tailwindcss/postcss": "^4.3.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1",
+ "@vkontakte/vk-miniapps-deploy": "^1.0.2",
"autoprefixer": "^10.5.0",
"eslint": "^10.3.0",
"eslint-plugin-react-hooks": "^7.1.1",
diff --git a/postcss.config.js b/postcss.config.js
new file mode 100644
index 0000000..3a29142
--- /dev/null
+++ b/postcss.config.js
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ '@tailwindcss/postcss': {}, // <--- ИСПРАВЛЕНО ПОД ТРЕБОВАНИЯ TAILWIND V4
+ autoprefixer: {},
+ },
+}
\ No newline at end of file
diff --git a/src/App.css b/src/App.css
index f90339d..b9d355d 100644
--- a/src/App.css
+++ b/src/App.css
@@ -1,184 +1,42 @@
-.counter {
- font-size: 16px;
- padding: 5px 10px;
- border-radius: 5px;
- color: var(--accent);
- background: var(--accent-bg);
- border: 2px solid transparent;
- transition: border-color 0.3s;
- margin-bottom: 24px;
+#root {
+ max-width: 1280px;
+ margin: 0 auto;
+ padding: 2rem;
+ text-align: center;
+}
- &:hover {
- border-color: var(--accent-border);
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
}
- &:focus-visible {
- outline: 2px solid var(--accent);
- outline-offset: 2px;
+ to {
+ transform: rotate(360deg);
}
}
-.hero {
- position: relative;
-
- .base,
- .framework,
- .vite {
- inset-inline: 0;
- margin: 0 auto;
- }
-
- .base {
- width: 170px;
- position: relative;
- z-index: 0;
- }
-
- .framework,
- .vite {
- position: absolute;
- }
-
- .framework {
- z-index: 1;
- top: 34px;
- height: 28px;
- transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg)
- scale(1.4);
- }
-
- .vite {
- z-index: 0;
- top: 107px;
- height: 26px;
- width: auto;
- transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg)
- scale(0.8);
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
}
}
-#center {
- display: flex;
- flex-direction: column;
- gap: 25px;
- place-content: center;
- place-items: center;
- flex-grow: 1;
-
- @media (max-width: 1024px) {
- padding: 32px 20px 24px;
- gap: 18px;
- }
+.card {
+ padding: 2em;
}
-#next-steps {
- display: flex;
- border-top: 1px solid var(--border);
- text-align: left;
-
- & > div {
- flex: 1 1 0;
- padding: 32px;
- @media (max-width: 1024px) {
- padding: 24px 20px;
- }
- }
-
- .icon {
- margin-bottom: 16px;
- width: 22px;
- height: 22px;
- }
-
- @media (max-width: 1024px) {
- flex-direction: column;
- text-align: center;
- }
-}
-
-#docs {
- border-right: 1px solid var(--border);
-
- @media (max-width: 1024px) {
- border-right: none;
- border-bottom: 1px solid var(--border);
- }
-}
-
-#next-steps ul {
- list-style: none;
- padding: 0;
- display: flex;
- gap: 8px;
- margin: 32px 0 0;
-
- .logo {
- height: 18px;
- }
-
- a {
- color: var(--text-h);
- font-size: 16px;
- border-radius: 6px;
- background: var(--social-bg);
- display: flex;
- padding: 6px 12px;
- align-items: center;
- gap: 8px;
- text-decoration: none;
- transition: box-shadow 0.3s;
-
- &:hover {
- box-shadow: var(--shadow);
- }
- .button-icon {
- height: 18px;
- width: 18px;
- }
- }
-
- @media (max-width: 1024px) {
- margin-top: 20px;
- flex-wrap: wrap;
- justify-content: center;
-
- li {
- flex: 1 1 calc(50% - 8px);
- }
-
- a {
- width: 100%;
- justify-content: center;
- box-sizing: border-box;
- }
- }
-}
-
-#spacer {
- height: 88px;
- border-top: 1px solid var(--border);
- @media (max-width: 1024px) {
- height: 48px;
- }
-}
-
-.ticks {
- position: relative;
- width: 100%;
-
- &::before,
- &::after {
- content: '';
- position: absolute;
- top: -4.5px;
- border: 5px solid transparent;
- }
-
- &::before {
- left: 0;
- border-left-color: var(--border);
- }
- &::after {
- right: 0;
- border-right-color: var(--border);
- }
+.read-the-docs {
+ color: #888;
}
diff --git a/src/App.jsx b/src/App.jsx
index fa09ed9..19ec039 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -1,113 +1,183 @@
import { useState, useEffect } from 'react';
-import { BrowserRouter as Router, Routes, Route, Link, useNavigate } from 'react-router-dom';
+import { HashRouter as Router, Routes, Route, useNavigate, useLocation } from 'react-router-dom';
+import { GoogleOAuthProvider } from '@react-oauth/google';
import bridge from '@vkontakte/vk-bridge';
-import axios from 'axios';
-import { Cpu, User, Loader2 } from 'lucide-react';
+import { motion, AnimatePresence } from 'framer-motion';
+import { Cpu, Globe, ShoppingCart, User, Mail, Sun, Moon } from 'lucide-react';
+import './index.css';
-import VkDashboard from './pages/VkDashboard';
-import VkPayment from './pages/VkPayment';
-import VkKabinet from './pages/VkKabinet';
+// ИМПОРТЫ ВАШИХ ОРИГИНАЛЬНЫХ СТРАНИЦ И КОМПОНЕНТОВ
+import Login from './components/Login';
+import Home from './pages/Home';
+import About from './pages/About';
+import Contact from './pages/Contact';
+import Dashboard from './pages/Dashboard';
+import Kabinet from './pages/Kabinet';
+import Payment from './pages/Payment';
-const API_URL = 'https://diplomnexus.aptcloud.ru';
+const clientId = '631083577297-n17acu7qspb1n9n8lhmr8q43b4vbpif1.apps.googleusercontent.com';
function AppContent() {
- const [user, setUser] = useState(null);
- const [loading, setLoading] = useState(true);
- const [error, setError] = useState(null);
+ const [isAuthenticated, setIsAuthenticated] = useState(!!localStorage.getItem('token'));
+ const [user, setUser] = useState(JSON.parse(localStorage.getItem('userInfo')) || null);
+ const [showLogin, setShowLogin] = useState(false);
+ const [showRegister, setShowRegister] = useState(false);
+
+ // Управление темой (Светлая по умолчанию)
+ const [theme, setTheme] = useState(localStorage.getItem('theme') || 'light');
+
const navigate = useNavigate();
+ const location = useLocation();
useEffect(() => {
- const autoLogin = async () => {
- const searchParams = new URLSearchParams(window.location.search);
- const isVk = searchParams.has('vk_user_id') && searchParams.has('sign');
+ document.body.className = theme;
+ localStorage.setItem('theme', theme);
+ }, [theme]);
- if (!isVk) {
- setLoading(false);
- setError('Пожалуйста, запустите приложение внутри ВКонтакте (VK Mini Apps).');
- return;
- }
+ // Запуск ВК-моста
+ useEffect(() => {
+ bridge.send('VKWebAppInit');
+ }, []);
- try {
- // Получаем нативные данные профиля от VK
- const vkUser = await bridge.send('VKWebAppGetUserInfo');
+ const handleAuth = (userInfo) => {
+ setIsAuthenticated(true);
+ setUser(userInfo);
+ localStorage.setItem('userInfo', JSON.stringify(userInfo));
+ setShowLogin(false);
+ setShowRegister(false);
+ navigate('/dashboard');
+ };
- // Собираем параметры запуска для подписи
- const launchParams = {};
- searchParams.forEach((value, key) => {
- if (key.startsWith('vk_') || key === 'sign') {
- launchParams[key] = value;
- }
- });
+ const handleLogout = () => {
+ setIsAuthenticated(false);
+ setUser(null);
+ localStorage.clear();
+ navigate('/');
+ };
- // Отправляем на ваш основной бэкенд для проверки сигнатуры и авторизации
- const res = await axios.post(`${API_URL}/auth/vk`, {
- launchParams,
- userInfo: vkUser
- });
-
- const { token, user: appUser } = res.data;
-
- localStorage.setItem('token', token);
- localStorage.setItem('userInfo', JSON.stringify(appUser));
-
- setUser(appUser);
- setLoading(false);
- navigate('/'); // Переходим в каталог
- } catch (err) {
- console.error('Ошибка авторизации VK:', err);
- setError('Не удалось войти через ВКонтакте. Проверьте настройки бэкенда.');
- setLoading(false);
- }
- };
-
- autoLogin();
- }, [navigate]);
-
- if (loading) {
- return (
-
-
-
NEXUS SECURE CONNECTING...
-
- );
- }
-
- if (error) {
- return (
-
-
⚠️ ОШИБКА ИНИЦИАЛИЗАЦИИ
-
{error}
-
- );
- }
+ // Логика перехода в приватные разделы
+ const handleProtectedNavigation = (path) => {
+ if (isAuthenticated) {
+ navigate(path);
+ } else {
+ setShowLogin(true);
+ }
+ };
+ const handleProfileClick = () => {
+ if (isAuthenticated) {
+ navigate('/kabinet');
+ } else {
+ setShowLogin(true);
+ }
+ };
return (
-
-
- } />
- } />
- } />
-
+
+
+ {/* КНОПКА СМЕНЫ ТЕМЫ (ПАРЯЩАЯ ВВЕРХУ СПРАВА) */}
+
+ setTheme(theme === 'dark' ? 'light' : 'dark')}
+ className="glass p-3 rounded-full flex items-center justify-center text-[var(--text-color)] transition-transform active:scale-95"
+ title="Сменить тему"
+ >
+ {theme === 'dark' ? : }
+
+
+
+ {/* ФОРМА АВТОРИЗАЦИИ (Login.jsx) */}
+
{ setShowLogin(false); setShowRegister(false); }}
+ onSuccess={handleAuth}
+ onSwitchToReg={() => { setShowLogin(false); setShowRegister(true); }}
+ onSwitchToLogin={() => { setShowRegister(false); setShowLogin(true); }}
+ />
+
+ {/* МАРШРУТИЗАЦИЯ СТРАНИЦ */}
+
+
+ } />
+ } />
+ } />
+
+ {/* Защищенные разделы */}
+ } />
+ } />
+ } />
+
+
+
+ {/* ПАРИРУЮЩИЙ BOTTOM NAV НА 5 КНОПОК (Стиль Akenai VPN) */}
+
+
+ {/* Кнопка: ГЛАВНАЯ */}
+ navigate('/')}
+ className={`vk-nav-btn ${location.pathname === '/' ? 'active' : ''}`}
+ >
+ {location.pathname === '/' && (
+
+ )}
+
+
+
+ {/* Кнопка: О НАС */}
+ navigate('/about')}
+ className={`vk-nav-btn ${location.pathname === '/about' ? 'active' : ''}`}
+ >
+ {location.pathname === '/about' && (
+
+ )}
+
+
+
+ {/* Кнопка: КАТАЛОГ */}
+ handleProtectedNavigation('/dashboard')}
+ className={`vk-nav-btn ${location.pathname === '/dashboard' ? 'active' : ''}`}
+ >
+ {location.pathname === '/dashboard' && (
+
+ )}
+
+
+
+ {/* Кнопка: КОНТАКТЫ */}
+ navigate('/contact')}
+ className={`vk-nav-btn ${location.pathname === '/contact' ? 'active' : ''}`}
+ >
+ {location.pathname === '/contact' && (
+
+ )}
+
+
+
+ {/* Кнопка: ЛИЧНЫЙ КАБИНЕТ */}
+
+ {location.pathname === '/kabinet' && (
+
+ )}
+
+
+
- {/* Мобильный док-бар */}
-
-
-
- Каталог
-
-
-
- Кабинет
-
-
);
}
export default function App() {
return (
-
-
-
+
+
+
+
+
);
}
\ No newline at end of file
diff --git a/src/assets/hero.png b/src/assets/hero.png
deleted file mode 100644
index 02251f4..0000000
Binary files a/src/assets/hero.png and /dev/null differ
diff --git a/src/assets/vite.svg b/src/assets/vite.svg
deleted file mode 100644
index 5101b67..0000000
--- a/src/assets/vite.svg
+++ /dev/null
@@ -1 +0,0 @@
-
Vite
diff --git a/src/components/ActionMenu.jsx b/src/components/ActionMenu.jsx
new file mode 100644
index 0000000..f972c7a
--- /dev/null
+++ b/src/components/ActionMenu.jsx
@@ -0,0 +1,122 @@
+import React, { useState, useRef, useEffect } from 'react';
+import styled from 'styled-components';
+import { Pencil, ShieldPlus, Settings, Trash2, MoreVertical } from 'lucide-react';
+
+const ActionMenu = ({ onEdit, onDelete }) => {
+ const [isOpen, setIsOpen] = useState(false);
+ const menuRef = useRef(null);
+
+ // Закрытие при клике вне компонента
+ useEffect(() => {
+ function handleClickOutside(event) {
+ if (menuRef.current && !menuRef.current.contains(event.target)) {
+ setIsOpen(false);
+ }
+ }
+ document.addEventListener("mousedown", handleClickOutside);
+ return () => document.removeEventListener("mousedown", handleClickOutside);
+ }, [menuRef]);
+
+ const handleAction = (action) => {
+ action();
+ setIsOpen(false);
+ };
+
+ return (
+
+ {/* КНОПКА ТРИ ТОЧКИ */}
+
setIsOpen(!isOpen)}
+ className="p-2 text-gray-400 hover:text-white hover:bg-white/10 rounded-full transition-all"
+ >
+
+
+
+ {/* ВЫПАДАЮЩЕЕ МЕНЮ */}
+ {isOpen && (
+
+
+
+ {/* РЕДАКТИРОВАТЬ */}
+ handleAction(onEdit)}>
+
+ Изменить
+
+
+
+
+ {/* УДАЛИТЬ */}
+ handleAction(onDelete)}>
+
+ Удалить
+
+
+
+
+ )}
+
+ );
+};
+
+// ТВОЙ STYLED COMPONENT (с небольшими правками позиционирования)
+const MenuContainer = styled.div`
+ .card {
+ width: 180px;
+ background-color: rgba(36, 40, 50, 1);
+ background-image: linear-gradient(139deg, rgba(36, 40, 50, 1) 0%, rgba(36, 40, 50, 1) 0%, rgba(37, 28, 40, 1) 100%);
+ border-radius: 10px;
+ padding: 10px 0px;
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ border: 1px solid rgba(255,255,255,0.1);
+ box-shadow: 0 10px 30px rgba(0,0,0,0.5);
+ }
+
+ .card .separator {
+ border-top: 1px solid #42434a;
+ margin: 4px 0;
+ }
+
+ .card .list {
+ list-style-type: none;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ padding: 0px 8px;
+ }
+
+ .card .list .element {
+ display: flex;
+ align-items: center;
+ color: #9ca3af;
+ gap: 10px;
+ transition: all 0.2s ease-out;
+ padding: 8px 10px;
+ border-radius: 6px;
+ cursor: pointer;
+ }
+
+ .card .list .element .label {
+ font-weight: 500;
+ font-size: 13px;
+ line-height: 1;
+ }
+
+ .card .list .element:hover {
+ background-color: var(--accent-color, #5353ff);
+ color: #ffffff;
+ transform: translateX(2px);
+ }
+
+ .card .list .delete:hover {
+ background-color: #ef4444; /* Красный tailwind */
+ color: white;
+ }
+
+ .card .list .element:active {
+ transform: scale(0.98);
+ }
+`;
+
+export default ActionMenu;
\ No newline at end of file
diff --git a/src/components/ArcReactor.jsx b/src/components/ArcReactor.jsx
new file mode 100644
index 0000000..a8e182c
--- /dev/null
+++ b/src/components/ArcReactor.jsx
@@ -0,0 +1,132 @@
+import React from 'react';
+import styled from 'styled-components';
+
+const ArcReactor = () => {
+ return (
+
+
+
+ {/* LAYER 1 (Внешнее кольцо) */}
+
+
+
+
+
+
+
+ {/* LAYER 2 (Вращающиеся элементы) */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* LAYER 4 (Центр) */}
+
+
+
+
+
+
+
+ );
+};
+
+const StyledWrapper = styled.div`
+ .svg-frame {
+ position: relative;
+ width: 300px;
+ height: 300px;
+ transform-style: preserve-3d;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ }
+
+ .svg-frame svg {
+ position: absolute;
+ transition: .5s;
+ z-index: calc(1 - (0.2 * var(--j)));
+ transform-origin: center;
+ width: 344px;
+ height: 344px;
+ fill: none;
+ /* Тень, чтобы линии было видно на белом */
+ filter: drop-shadow(0 0 5px var(--accent-color));
+ }
+
+ /* Цвета с использованием CSS переменных */
+ .accent-stroke {
+ stroke: var(--accent-color);
+ }
+ .accent-fill {
+ fill: var(--accent-color);
+ }
+
+ /* ЦЕНТР ЖЕЛТЫЙ (#center1) */
+ #center1 {
+ fill: #ffff00; /* Яркий желтый */
+ stroke: #000; /* Черная обводка для контраста на белом */
+ stroke-width: 1px;
+ animation: rotate16 2s ease-in-out infinite alternate;
+ transform-origin: center;
+ }
+
+ /* Внутренний центр */
+ #center {
+ fill: var(--accent-color);
+ transition: .5s;
+ transform-origin: center;
+ }
+
+ .svg-frame:hover svg {
+ transform: rotate(-80deg) skew(30deg) translateX(calc(45px * var(--i))) translateY(calc(-35px * var(--i)));
+ }
+
+ .svg-frame:hover svg #center {
+ transform: rotate(-30deg) translateX(45px) translateY(-3px);
+ }
+
+ #out2 {
+ animation: rotate16 7s ease-in-out infinite alternate;
+ transform-origin: center;
+ }
+
+ @keyframes rotate16 {
+ to {
+ transform: rotate(360deg);
+ }
+ }
+`;
+
+export default ArcReactor;
\ No newline at end of file
diff --git a/src/components/Fingerprint.jsx b/src/components/Fingerprint.jsx
new file mode 100644
index 0000000..9395983
--- /dev/null
+++ b/src/components/Fingerprint.jsx
@@ -0,0 +1,34 @@
+import React from 'react';
+import styled from 'styled-components';
+
+const Fingerprint = () => {
+ return (
+
+
+
+
+
+
+
+
+
Verifying...
+
+
+ );
+}
+
+const StyledWrapper = styled.div`
+ .fingerprint-container { position: relative; width: 120px; height: 120px; cursor: pointer; border-radius: 50%; margin: 0 auto; }
+ .fingerprint-svg { width: 100%; height: 100%; color: #00ff00; filter: drop-shadow(0 0 5px #00ff00); transition: transform 0.2s ease; }
+ .fingerprint-path { stroke-dasharray: 500; stroke-dashoffset: 0; animation: draw 4s infinite linear; }
+ .scan-line { position: absolute; top: 0; left: 0; width: 100%; height: 3px; background: linear-gradient(to right, transparent, #00ff00, transparent); opacity: 0; }
+ .status { position: absolute; bottom: -30px; width: 100%; text-align: center; color: #00ff00; font-size: 14px; text-transform: uppercase; letter-spacing: 2px; animation: glitch-text 2s infinite; }
+
+ .fingerprint-container:hover .scan-line { animation: scan 1s infinite linear; opacity: 0.7; }
+
+ @keyframes draw { 0% { stroke-dashoffset: 500; } 100% { stroke-dashoffset: 0; } }
+ @keyframes scan { 0% { transform: translateY(0); opacity: 0.7; } 50% { opacity: 1; } 100% { transform: translateY(120px); opacity: 0.7; } }
+ @keyframes glitch-text { 0% { transform: translate(0); } 20% { transform: translate(-1px, 1px); } 40% { transform: translate(1px, -1px); } 60% { transform: translate(-1px, 0); } 80% { transform: translate(1px, 0); } 100% { transform: translate(0); } }
+`;
+
+export default Fingerprint;
\ No newline at end of file
diff --git a/src/components/Iphone.jsx b/src/components/Iphone.jsx
new file mode 100644
index 0000000..6bedcda
--- /dev/null
+++ b/src/components/Iphone.jsx
@@ -0,0 +1,70 @@
+import React from 'react';
+import styled from 'styled-components';
+
+const Iphone = () => {
+ return (
+
+
+
+ );
+}
+
+const StyledWrapper = styled.div`
+ .card {
+ width: 210px; height: 400px; background: black; border-radius: 35px;
+ border: 2px solid rgb(40, 40, 40); padding: 7px; position: relative;
+ box-shadow: 2px 5px 15px rgba(0, 0, 0, 0.486); margin: 0 auto;
+ }
+ .card-int {
+ background-image: linear-gradient(to right bottom, #ff0000, #ff0045, #ff0078, #ea00aa, #b81cd7, #8a3ad6, #5746cf, #004ac2, #003d94, #002e66, #001d3a, #020812);
+ background-size: 200% 200%; background-position: 0% 0%; height: 100%; border-radius: 25px;
+ transition: all 0.6s ease-out; overflow: hidden;
+ }
+ .card:hover .card-int { background-position: 100% 100%; }
+ .top {
+ position: absolute; top: 0px; right: 50%; transform: translate(50%, 0%);
+ width: 35%; height: 18px; background-color: black;
+ border-bottom-left-radius: 10px; border-bottom-right-radius: 10px;
+ }
+ .speaker {
+ position: absolute; top: 2px; right: 50%; transform: translate(50%, 0%);
+ width: 40%; height: 2px; border-radius: 2px; background-color: rgb(20, 20, 20);
+ }
+ .camera {
+ position: absolute; top: 6px; right: 84%; transform: translate(50%, 0%);
+ width: 6px; height: 6px; border-radius: 50%; background-color: rgba(255, 255, 255, 0.048);
+ }
+ .int {
+ position: absolute; width: 3px; height: 3px; border-radius: 50%;
+ top: 50%; right: 50%; transform: translate(50%, -50%); background-color: rgba(0, 0, 255, 0.212);
+ }
+ .btn1, .btn2, .btn3, .btn4 { position: absolute; width: 2px; background-image: linear-gradient(to right, #111111, #222222, #333333, #464646, #595959); }
+ .btn1 { height: 45px; top: 30%; right: -4px; }
+ .btn2 { height: 30px; top: 26%; left: -4px; }
+ .btn3 { height: 30px; top: 36%; left: -4px; }
+ .btn4 { height: 45px; top: 11%; right: -4px; } /* Added missing btn4 style for completion */
+ .hello {
+ display: flex; flex-flow: column; align-items: center; justify-content: center;
+ color: white; font-size: 2rem; font-weight: bold; text-align: center;
+ line-height: 35px; height: 100%; transition: 0.5s ease-in-out;
+ }
+ .hidden { display: block; opacity: 0; transition: all 0.3s ease-in; font-size: 1rem; }
+ .card:hover .hidden { opacity: 1; }
+ .card:hover .hello { transform: translateY(-20px); }
+`;
+
+export default Iphone;
diff --git a/src/components/Login.jsx b/src/components/Login.jsx
new file mode 100644
index 0000000..ddb1ec4
--- /dev/null
+++ b/src/components/Login.jsx
@@ -0,0 +1,352 @@
+import { useState } from 'react';
+import axios from 'axios';
+import { motion, AnimatePresence } from 'framer-motion';
+import { X, User, Lock, Mail, Fingerprint, ScanFace, ArrowRight } from 'lucide-react';
+import { useGoogleLogin } from '@react-oauth/google';
+import bridge from '@vkontakte/vk-bridge';
+import GoogleButton from './google';
+
+const Login = ({ showLogin, showRegister, onClose, onSuccess, onSwitchToReg, onSwitchToLogin }) => {
+ const [email, setEmail] = useState('');
+ const [password, setPassword] = useState('');
+ const [name, setName] = useState('');
+ const [error, setError] = useState('');
+ const [loading, setLoading] = useState(false);
+ const [step, setStep] = useState('auth'); // 'auth' или 'verify'
+ const [verificationCode, setVerificationCode] = useState('');
+
+ // Логика Google входа
+ const googleLogin = useGoogleLogin({
+ onSuccess: async (tokenResponse) => {
+ try {
+ const res = await axios.post('https://diplomnexus.aptcloud.ru/auth/google', {
+ access_token: tokenResponse.access_token,
+ });
+ localStorage.setItem('token', res.data.token);
+ onSuccess(res.data.user);
+ } catch (err) {
+ setError('Ошибка Google авторизации');
+ }
+ },
+ onError: () => setError('Google вход не удался'),
+ });
+ const handleVkLogin = async () => {
+ setError('');
+ setLoading(true);
+ try {
+ const searchParams = new URLSearchParams(window.location.search);
+ const launchParams = {};
+ searchParams.forEach((value, key) => {
+ if (key.startsWith('vk_') || key === 'sign') {
+ launchParams[key] = value;
+ }
+ });
+
+ const vkUser = await bridge.send('VKWebAppGetUserInfo');
+
+ const res = await axios.post('https://diplomnexus.aptcloud.ru/auth/vk', {
+ launchParams,
+ userInfo: vkUser
+ });
+
+ localStorage.setItem('token', res.data.token);
+ onSuccess(res.data.user);
+ } catch (err) {
+ console.error(err);
+ setError('Не удалось авторизоваться через ВКонтакте');
+ } finally {
+ setLoading(false);
+ }
+ };
+ const handleSubmit = async (e, isRegister) => {
+ e.preventDefault();
+ setError('');
+ setLoading(true);
+
+ try {
+ await new Promise(resolve => setTimeout(resolve, 800));
+
+ if (isRegister && step === 'auth') {
+ // Шаг 1: отправка данных для регистрации и генерации кода на почту
+ const endpoint = 'https://diplomnexus.aptcloud.ru/register';
+ const payload = { email, password, name, referral_code: localStorage.getItem('referral_code') };
+
+ await axios.post(endpoint, payload);
+ setStep('verify'); // Переключаемся на окно ввода кода
+ } else if (isRegister && step === 'verify') {
+ // Шаг 2: отправка кода верификации
+ const endpoint = 'https://diplomnexus.aptcloud.ru/verify';
+ const payload = { email, code: verificationCode };
+
+ const res = await axios.post(endpoint, payload);
+ localStorage.setItem('token', res.data.token);
+ onSuccess(res.data.user);
+ } else {
+ // Обычный вход в систему (Авторизация)
+ const endpoint = 'https://diplomnexus.aptcloud.ru/login';
+ const payload = { email, password };
+
+ const res = await axios.post(endpoint, payload);
+ localStorage.setItem('token', res.data.token);
+ onSuccess(res.data.user);
+ }
+ } catch (err) {
+ setError(err.response?.data?.error || err.response?.data?.message || 'Ошибка доступа: Неверные данные');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ // Возврат на шаг назад (если ввели не ту почту)
+ const handleBackToAuth = () => {
+ setStep('auth');
+ setError('');
+ setVerificationCode('');
+ };
+
+ const handleSwitchToLogin = () => {
+ setStep('auth');
+ setError('');
+ onSwitchToLogin();
+ };
+
+ const handleSwitchToReg = () => {
+ setStep('auth');
+ setError('');
+ onSwitchToReg();
+ };
+
+ if (!showLogin && !showRegister) return null;
+
+ const isRegister = showRegister;
+
+ return (
+
+ {/* Убрал onClick={onClose} отсюда. Теперь клик по фону ничего не делает */}
+
+
+ {/* ФОНОВЫЕ ЭФФЕКТЫ */}
+
+
+
e.stopPropagation()}
+ >
+ {/* ОСНОВНОЙ КОНТЕЙНЕР (СТЕКЛО) */}
+
+
+ {/* ВЕРХНЯЯ ПОЛОСА ЗАГРУЗКИ */}
+ {loading && (
+
+ )}
+
+
+
+ {/* === КРЕСТИК (ЗАКРЫВАЕТ ТОЛЬКО ОН) === */}
+
+
+
+
+ {/* ДЕКОРАТИВНЫЕ УГОЛКИ */}
+
+
+
+
+ {/* ЗАГОЛОВОК */}
+
+
+
+ {isRegister && step === 'verify' ? (
+
+ ) : isRegister ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ {isRegister && step === 'verify' ? 'Верификация' : isRegister ? 'Инициализация' : 'Вход в систему'}
+
+
+ SECURE CONNECTION ESTABLISHED
+
+
+
+ {/* ОШИБКА */}
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+ {/* ФОРМА */}
+
+
+ {/* РАЗДЕЛИТЕЛЬ */}
+ {!(isRegister && step === 'verify') && (
+ <>
+ {/* РАЗДЕЛИТЕЛЬ */}
+
+
+ Или через соц.сети
+
+
+
+
+
+ googleLogin()} />
+
+ Войти через ВКонтакте
+
+
+
+ >
+ )}
+
+ {/* ПЕРЕКЛЮЧАТЕЛЬ */}
+
+
+ {isRegister ? 'Уже есть доступ?' : 'Нет идентификатора?'}
+
+
+ {isRegister ? 'Войти в систему' : 'Регистрация'}
+
+
+
+
+
+
+
+
+ );
+};
+
+export default Login;
diff --git a/src/components/Mesto.jsx b/src/components/Mesto.jsx
new file mode 100644
index 0000000..4ecbc1b
--- /dev/null
+++ b/src/components/Mesto.jsx
@@ -0,0 +1,281 @@
+import React from 'react';
+import styled from 'styled-components';
+
+const Mesto = () => {
+ return (
+
+
+
+ {/* ФОН КАРТЫ */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* === СЛОЙ 1: ЛАНДШАФТ === */}
+
+
+
+ {/* Дорожки */}
+ {/* Парковка */}
+
+ {/* Патио */}
+
+ {/* === СЛОЙ 2: СТРОЕНИЯ === */}
+
+ {/* ГАРАЖ */}
+
+
+
+
+
+
+ {/* ДОМ */}
+
+
+
+
+
+
+
+
+ {/* БАССЕЙН */}
+
+
+ {/* === СЛОЙ 3: ОБЪЕКТЫ === */}
+
+ {/* МАШИНА */}
+
+
+
+
+
+
+
+ {/* МАНГАЛЬНАЯ ЗОНА (Отодвинул вниз на траву - координаты 500, 300) */}
+
+ {/* Плитка под мангал */}
+
+ {/* Сам мангал */}
+
+
+
+ {/* Дым */}
+
+
+
+
+
+
+ {/* ЧЕЛОВЕК (Вид СВЕРХУ) - Сдвинул ближе к центру дорожки */}
+
+
+
+
+
+ {/* СОБАКА (Вид СВЕРХУ) */}
+
+
+
+
+
+
+ {/* ДЕРЕВЬЯ */}
+
+
+
+
+
+
+ {/* === ПИНЫ ДАТЧИКОВ === */}
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* ЧЕЛОВЕК: Точка ровно над головой (320/600=53.3%, 380/500=76%) */}
+
+
+
+
+ {/* СОЛНЕЧНЫЕ ПАНЕЛИ */}
+
+
Электричество: 4.2 kW
+
+
+
+
+ {/* МАНГАЛ: Точка над мангалом (500/600=83.3%, 300/500=60%) */}
+
+
+
+
+
+
+ {/* ДАТЧИК ВЛАЖНОСТИ (ВЕРНУЛ) */}
+
+
+
+
+
+ );
+}
+
+const StyledWrapper = styled.div`
+ width: 100%;
+ display: flex;
+ justify-content: center;
+
+ .map-container {
+ --city-sign-color-back: rgba(15, 23, 42, 0.95);
+ --city-sign-color-font: #fff;
+ position: relative;
+ width: 100%;
+ max-width: 650px;
+ aspect-ratio: 4 / 3;
+ background: transparent;
+ }
+
+ .map-background-wrapper {
+ position: absolute;
+ top: 0; left: 0; width: 100%; height: 100%;
+ border-radius: 20px;
+ overflow: hidden;
+ box-shadow: 0 20px 50px rgba(0,0,0,0.4), inset 0 0 0 2px rgba(255,255,255,0.1);
+ background: #7ec850;
+ z-index: 1;
+ }
+
+ .map-svg { width: 100%; height: 100%; object-fit: cover; }
+ .map-cities { width: 100%; height: 100%; position: relative; z-index: 10; overflow: visible; }
+
+ /* ПИНЫ (Маленькие точки) */
+ .map-city {
+ position: absolute;
+ left: calc(var(--x) * 1%);
+ top: calc(var(--y) * 1%);
+ width: 12px; height: 12px;
+ background: #ef4444;
+ border: 2px solid white;
+ border-radius: 50%;
+ box-shadow: 0 2px 4px rgba(0,0,0,0.3);
+ transform: translate(-50%, -50%);
+ cursor: pointer;
+ transition: all 0.2s ease;
+ z-index: 20;
+ }
+
+ .map-city[data-status="active"] { background: #22c55e; }
+ .map-city[data-status="normal"] { background: #3b82f6; }
+ .map-city[data-status="warning"] { background: #f59e0b; animation: pulse-warning 1s infinite; }
+
+ .map-city.main-hub {
+ width: 18px; height: 18px;
+ background: #a855f7;
+ border-radius: 4px;
+ transform: translate(-50%, -50%) rotate(45deg);
+ }
+
+ .map-city:hover {
+ transform: translate(-50%, -50%) scale(1.5);
+ z-index: 100;
+ border-color: #fbbf24;
+ }
+
+ .map-city__label {
+ opacity: 0;
+ visibility: hidden;
+ position: absolute;
+ bottom: 20px;
+ left: 50%;
+ transform: translateX(-50%) translateY(5px);
+ white-space: nowrap;
+ z-index: 50;
+ transition: all 0.2s ease;
+ pointer-events: none;
+ }
+
+ .map-city:hover .map-city__label {
+ opacity: 1;
+ visibility: visible;
+ transform: translateX(-50%) translateY(0);
+ }
+
+ .map-city__sign {
+ background: var(--city-sign-color-back);
+ color: var(--city-sign-color-font);
+ padding: 4px 8px;
+ border-radius: 4px;
+ font-size: 10px;
+ font-family: 'Inter', sans-serif;
+ font-weight: 600;
+ display: flex; align-items: center; gap: 6px;
+ box-shadow: 0 4px 10px rgba(0,0,0,0.5);
+ border: 1px solid rgba(255,255,255,0.2);
+ }
+
+ .map-city__sign::before { content: attr(data-icon); font-size: 12px; }
+
+ .map-city__sign::after {
+ content: ''; position: absolute; bottom: -4px; left: 50%;
+ transform: translateX(-50%);
+ border-left: 4px solid transparent; border-right: 4px solid transparent;
+ border-top: 4px solid var(--city-sign-color-back);
+ }
+
+ @keyframes pulse-warning {
+ 0% { box-shadow: 0 0 0 0 rgba(245, 158, 11, 0.7); }
+ 70% { box-shadow: 0 0 0 6px rgba(245, 158, 11, 0); }
+ 100% { box-shadow: 0 0 0 0 rgba(245, 158, 11, 0); }
+ }
+`;
+
+export default Mesto;
\ No newline at end of file
diff --git a/src/components/NavBar.jsx b/src/components/NavBar.jsx
new file mode 100644
index 0000000..8ecc684
--- /dev/null
+++ b/src/components/NavBar.jsx
@@ -0,0 +1,188 @@
+import React, { useState, useEffect } from 'react';
+import axios from 'axios';
+import { Link, useNavigate, useLocation } from 'react-router-dom';
+import { motion } from 'framer-motion';
+import { Cpu, LogOut, User, MessageCircle } from 'lucide-react';
+import LightDark from './lightdark';
+import UserAvatar from './UserAvatar';
+import SupportChat from './SupportChat';
+
+const navLinks = [
+ { path: '/', label: 'ГЛАВНАЯ' },
+ { path: '/about', label: 'О НАС' },
+ { path: '/contact', label: 'КОНТАКТЫ' },
+];
+
+const catalogLink = [
+ { path: '/dashboard', label: 'КАТАЛОГ' }
+];
+
+const NavBar = ({ user, onLogin, onLogout }) => {
+ const [theme, setTheme] = useState(localStorage.getItem('theme') || 'dark');
+ const [hoveredPath, setHoveredPath] = useState(null);
+ const [unreadCount, setUnreadCount] = useState(0);
+ const [isChatOpen, setIsChatOpen] = useState(false);
+ const navigate = useNavigate();
+ const location = useLocation();
+
+ const isAdmin = user?.role === 'admin';
+ const roleLabel = isAdmin ? 'ADMIN' : 'OPERATOR';
+ const roleColorClass = isAdmin ? 'text-red-500' : 'text-[var(--accent-color)]';
+ const adminColorClass = 'text-red-500';
+
+ // --- 1. ТАБЛИЦЫ ОБОРУДОВАНИЯ (ЧИСТЫЕ ПУТИ ДЛЯ ВСЕХ) ---
+ const equipmentLinks = [
+ { path: '/hubs', label: 'ХАБЫ' },
+ { path: '/cameras', label: 'КАМЕРЫ' },
+ { path: '/lighting', label: 'СВЕТ' },
+ { path: '/sensors', label: 'ДАТЧИКИ' },
+ ];
+
+ // --- 2. ТАБЛИЦЫ УПРАВЛЕНИЯ (ТОЛЬКО АДМИНУ) ---
+ const adminManagementLinks = [
+ { path: '/users', label: 'ПОЛЬЗОВАТЕЛИ' },
+ { path: '/orders', label: 'ЗАКАЗЫ' },
+ { path: '/messages', label: 'ЧАТЫ' },
+ { path: '/admin/logs', label: 'ЛОГИ' },
+ ];
+
+ useEffect(() => {
+ document.body.className = theme;
+ localStorage.setItem('theme', theme);
+ }, [theme]);
+
+ useEffect(() => {
+ if (user) {
+ const checkUnread = async () => {
+ try {
+ const token = localStorage.getItem('token');
+ const res = await axios.get(`https://diplomnexus.aptcloud.ru/messages/unread?email=${user.email}`, {
+ headers: { Authorization: `Bearer ${token}` }
+ });
+ setUnreadCount(res.data.count);
+ } catch (e) {}
+ };
+ checkUnread();
+ const interval = setInterval(checkUnread, 5000);
+ return () => clearInterval(interval);
+ }
+ }, [user]);
+
+ const toggleTheme = () => setTheme(theme === 'dark' ? 'light' : 'dark');
+
+ const renderMenu = (links) => (
+
+ {links.map((link) => (
+
setHoveredPath(link.path)}
+ onMouseLeave={() => setHoveredPath(null)}
+ className="relative px-3 xl:px-4 py-3 text-[9px] xl:text-[10px] font-bold tracking-widest text-[var(--text-color)] transition-colors hover:text-[var(--accent-color)] uppercase z-10 whitespace-nowrap"
+ >
+ {link.label}
+
+ {hoveredPath === link.path && (
+
+
+
+
+ )}
+
+ {location.pathname === link.path && (
+
+ )}
+
+ ))}
+
+ );
+
+ return (
+ <>
+
+
+
+
+
+
+
+
NEXUS
+
+
+
+ {renderMenu(navLinks)}
+
+ {user && (
+ <>
+
+ {renderMenu(catalogLink)}
+
+
+ {/* Группа таблиц оборудования доступна всем авторизованным */}
+ {renderMenu(equipmentLinks)}
+
+ {isAdmin && (
+ <>
+
+
+
+ ADMIN:
+
+ {renderMenu(adminManagementLinks)}
+
+ >
+ )}
+ >
+ )}
+
+
+
+
+
+
+
+ {user ? (
+
+
{ setIsChatOpen(!isChatOpen); setUnreadCount(0); }}
+ className={`p-2 rounded-full transition-all duration-300 relative ${isChatOpen ? 'bg-[var(--accent-color)] text-black' : 'text-[var(--text-color)] hover:text-[var(--accent-color)] bg-[var(--accent-color)]/10'}`}>
+
+ {unreadCount > 0 && }
+
+
+
navigate('/kabinet')} className="flex items-center gap-3 cursor-pointer group">
+
+ {roleLabel}
+ {user.name}
+
+
+
+
+
+
+
+
+ ) : (
+
+ Войти
+
+ )}
+
+
+
+
+ {user &&
setIsChatOpen(false)} user={user} />}
+ >
+ );
+};
+
+export default NavBar;
diff --git a/src/components/Notebook.jsx b/src/components/Notebook.jsx
new file mode 100644
index 0000000..58d0e5e
--- /dev/null
+++ b/src/components/Notebook.jsx
@@ -0,0 +1,553 @@
+import React from 'react';
+import styled from 'styled-components';
+
+const Notebook = () => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+const StyledWrapper = styled.div`
+ .macbook {
+ width: 150px;
+ height: 96px;
+ position: absolute;
+ left: 50%;
+ top: 50%;
+ margin: -85px 0 0 -78px;
+ perspective: 500px;
+ }
+
+ .shadow {
+ position: absolute;
+ width: 60px;
+ height: 0px;
+ left: 40px;
+ top: 160px;
+ transform: rotateX(80deg) rotateY(0deg) rotateZ(0deg);
+ box-shadow: 0 0 60px 40px rgba(0,0,0,0.3);
+ animation: shadow infinite 7s ease;
+ }
+
+ .inner {
+ z-index: 20;
+ position: absolute;
+ width: 150px;
+ height: 96px;
+ left: 0;
+ top: 0;
+ transform-style: preserve-3d;
+ transform: rotateX(-20deg) rotateY(0deg) rotateZ(0deg);
+ animation: rotate infinite 7s ease;
+ }
+
+ .screen {
+ width: 150px;
+ height: 96px;
+ position: absolute;
+ left: 0;
+ bottom: 0;
+ border-radius: 7px;
+ background: #ddd;
+ transform-style: preserve-3d;
+ transform-origin: 50% 93px;
+ transform: rotateX(0deg) rotateY(0deg) rotateZ(0deg);
+ animation: lid-screen infinite 7s ease;
+ background-image: linear-gradient(45deg, rgba(0,0,0,0.34) 0%,rgba(0,0,0,0) 100%);
+ background-position: left bottom;
+ background-size: 300px 300px;
+ box-shadow: inset 0 3px 7px rgba(255,255,255,0.5);
+ }
+
+ .screen .logo {
+ position: absolute;
+ width: 20px;
+ height: 24px;
+ left: 50%;
+ top: 50%;
+ margin: -12px 0 0 -10px;
+ transform: rotateY(180deg) translateZ(0.1px);
+ }
+
+ .screen .face-one {
+ width: 150px;
+ height: 96px;
+ position: absolute;
+ left: 0;
+ bottom: 0;
+ border-radius: 7px;
+ background: #d3d3d3;
+ transform: translateZ(2px);
+ background-image: linear-gradient(45deg,rgba(0,0,0,0.24) 0%,rgba(0,0,0,0) 100%);
+ }
+
+ .screen .face-one .camera {
+ width: 3px;
+ height: 3px;
+ border-radius: 100%;
+ background: #000;
+ position: absolute;
+ left: 50%;
+ top: 4px;
+ margin-left: -1.5px;
+ }
+
+ .screen .face-one .display {
+ width: 130px;
+ height: 74px;
+ margin: 10px;
+ background-color: #000;
+ background-size: 100% 100%;
+ border-radius: 1px;
+ position: relative;
+ box-shadow: inset 0 0 2px rgba(0,0,0,1);
+ }
+
+ .screen .face-one .display .shade {
+ position: absolute;
+ left: 0;
+ top: 0;
+ width: 130px;
+ height: 74px;
+ background: linear-gradient(-135deg, rgba(255,255,255,0) 0%,rgba(255,255,255,0.1) 47%,rgba(255,255,255,0) 48%);
+ animation: screen-shade infinite 7s ease;
+ background-size: 300px 200px;
+ background-position: 0px 0px;
+ }
+
+ .screen .face-one span {
+ position: absolute;
+ top: 85px;
+ left: 57px;
+ font-size: 6px;
+ color: #666
+ }
+
+ .macbody {
+ width: 150px;
+ height: 96px;
+ position: absolute;
+ left: 0;
+ bottom: 0;
+ border-radius: 7px;
+ background: #cbcbcb;
+ transform-style: preserve-3d;
+ transform-origin: 50% bottom;
+ transform: rotateX(-90deg);
+ animation: lid-macbody infinite 7s ease;
+ background-image: linear-gradient(45deg, rgba(0,0,0,0.24) 0%,rgba(0,0,0,0) 100%);
+ }
+
+ .macbody .face-one {
+ width: 150px;
+ height: 96px;
+ position: absolute;
+ left: 0;
+ bottom: 0;
+ border-radius: 7px;
+ transform-style: preserve-3d;
+ background: #dfdfdf;
+ animation: lid-keyboard-area infinite 7s ease;
+ transform: translateZ(-2px);
+ background-image: linear-gradient(30deg, rgba(0,0,0,0.24) 0%,rgba(0,0,0,0) 100%);
+ }
+
+ .macbody .touchpad {
+ width: 40px;
+ height: 31px;
+ position: absolute;
+ left: 50%;
+ top: 50%;
+ border-radius: 4px;
+ margin: -44px 0 0 -18px;
+ background: #cdcdcd;
+ background-image: linear-gradient(30deg, rgba(0,0,0,0.24) 0%,rgba(0,0,0,0) 100%);
+ box-shadow: inset 0 0 3px #888;
+ }
+
+ .macbody .keyboard {
+ width: 130px;
+ height: 45px;
+ position: absolute;
+ left: 7px;
+ top: 41px;
+ border-radius: 4px;
+ transform-style: preserve-3d;
+ background: #cdcdcd;
+ background-image: linear-gradient(30deg, rgba(0,0,0,0.24) 0%,rgba(0,0,0,0) 100%);
+ box-shadow: inset 0 0 3px #777;
+ padding: 0 0 0 2px;
+ }
+
+ .keyboard .key {
+ width: 6px;
+ height: 6px;
+ background: #444;
+ float: left;
+ margin: 1px;
+ transform: translateZ(-2px);
+ border-radius: 2px;
+ box-shadow: 0 -2px 0 #222;
+ animation: keys infinite 7s ease;
+ }
+
+ .key.space {
+ width: 45px;
+ }
+
+ .key.f {
+ height: 3px;
+ }
+
+ .macbody .pad {
+ width: 5px;
+ height: 5px;
+ background: #333;
+ border-radius: 100%;
+ position: absolute;
+ }
+
+ .pad.one {
+ left: 20px;
+ top: 20px;
+ }
+
+ .pad.two {
+ right: 20px;
+ top: 20px;
+ }
+
+ .pad.three {
+ right: 20px;
+ bottom: 20px;
+ }
+
+ .pad.four {
+ left: 20px;
+ bottom: 20px;
+ }
+
+ @keyframes rotate {
+ 0% {
+ transform: rotateX(-20deg) rotateY(0deg) rotateZ(0deg);
+ }
+
+ 5% {
+ transform: rotateX(-20deg) rotateY(-20deg) rotateZ(0deg);
+ }
+
+ 20% {
+ transform: rotateX(30deg) rotateY(200deg) rotateZ(0deg);
+ }
+
+ 25% {
+ transform: rotateX(-60deg) rotateY(150deg) rotateZ(0deg);
+ }
+
+ 60% {
+ transform: rotateX(-20deg) rotateY(130deg) rotateZ(0deg);
+ }
+
+ 65% {
+ transform: rotateX(-20deg) rotateY(120deg) rotateZ(0deg);
+ }
+
+ 80% {
+ transform: rotateX(-20deg) rotateY(375deg) rotateZ(0deg);
+ }
+
+ 85% {
+ transform: rotateX(-20deg) rotateY(357deg) rotateZ(0deg);
+ }
+
+ 87% {
+ transform: rotateX(-20deg) rotateY(360deg) rotateZ(0deg);
+ }
+
+ 100% {
+ transform: rotateX(-20deg) rotateY(360deg) rotateZ(0deg);
+ }
+ }
+
+ @keyframes lid-screen {
+ 0% {
+ transform: rotateX(0deg);
+ background-position: left bottom;
+ }
+
+ 5% {
+ transform: rotateX(50deg);
+ background-position: left bottom;
+ }
+
+ 20% {
+ transform: rotateX(-90deg);
+ background-position: -150px top;
+ }
+
+ 25% {
+ transform: rotateX(15deg);
+ background-position: left bottom;
+ }
+
+ 30% {
+ transform: rotateX(-5deg);
+ background-position: right top;
+ }
+
+ 38% {
+ transform: rotateX(5deg);
+ background-position: right top;
+ }
+
+ 48% {
+ transform: rotateX(0deg);
+ background-position: right top;
+ }
+
+ 90% {
+ transform: rotateX(0deg);
+ background-position: right top;
+ }
+
+ 100% {
+ transform: rotateX(0deg);
+ background-position: right center;
+ }
+ }
+
+ @keyframes lid-macbody {
+ 0% {
+ transform: rotateX(-90deg);
+ }
+
+ 50% {
+ transform: rotateX(-90deg);
+ }
+
+ 100% {
+ transform: rotateX(-90deg);
+ }
+ }
+
+ @keyframes lid-keyboard-area {
+ 0% {
+ background-color: #dfdfdf;
+ }
+
+ 50% {
+ background-color: #bbb;
+ }
+
+ 100% {
+ background-color: #dfdfdf;
+ }
+ }
+
+ @keyframes screen-shade {
+ 0% {
+ background-position: -20px 0px;
+ }
+
+ 5% {
+ background-position: -40px 0px;
+ }
+
+ 20% {
+ background-position: 200px 0;
+ }
+
+ 50% {
+ background-position: -200px 0;
+ }
+
+ 80% {
+ background-position: 0px 0px;
+ }
+
+ 85% {
+ background-position: -30px 0;
+ }
+
+ 90% {
+ background-position: -20px 0;
+ }
+
+ 100% {
+ background-position: -20px 0px;
+ }
+ }
+
+ @keyframes keys {
+ 0% {
+ box-shadow: 0 -2px 0 #222;
+ }
+
+ 5% {
+ box-shadow: 1 -1px 0 #222;
+ }
+
+ 20% {
+ box-shadow: -1px 1px 0 #222;
+ }
+
+ 25% {
+ box-shadow: -1px 1px 0 #222;
+ }
+
+ 60% {
+ box-shadow: -1px 1px 0 #222;
+ }
+
+ 80% {
+ box-shadow: 0 -2px 0 #222;
+ }
+
+ 85% {
+ box-shadow: 0 -2px 0 #222;
+ }
+
+ 87% {
+ box-shadow: 0 -2px 0 #222;
+ }
+
+ 100% {
+ box-shadow: 0 -2px 0 #222;
+ }
+ }
+
+ @keyframes shadow {
+ 0% {
+ transform: rotateX(80deg) rotateY(0deg) rotateZ(0deg);
+ box-shadow: 0 0 60px 40px rgba(0,0,0,0.3);
+ }
+
+ 5% {
+ transform: rotateX(80deg) rotateY(10deg) rotateZ(0deg);
+ box-shadow: 0 0 60px 40px rgba(0,0,0,0.3);
+ }
+
+ 20% {
+ transform: rotateX(30deg) rotateY(-20deg) rotateZ(-20deg);
+ box-shadow: 0 0 50px 30px rgba(0,0,0,0.3);
+ }
+
+ 25% {
+ transform: rotateX(80deg) rotateY(-20deg) rotateZ(50deg);
+ box-shadow: 0 0 35px 15px rgba(0,0,0,0.1);
+ }
+
+ 60% {
+ transform: rotateX(80deg) rotateY(0deg) rotateZ(-50deg) translateX(30px);
+ box-shadow: 0 0 60px 40px rgba(0,0,0,0.3);
+ }
+
+ 100% {
+ box-shadow: 0 0 60px 40px rgba(0,0,0,0.3);
+ }
+ }`;
+
+export default Notebook;
diff --git a/src/components/OsCore.jsx b/src/components/OsCore.jsx
new file mode 100644
index 0000000..f9bc613
--- /dev/null
+++ b/src/components/OsCore.jsx
@@ -0,0 +1,102 @@
+import React from 'react';
+import styled from 'styled-components';
+
+const OsCore = () => {
+ return (
+
+
+
+ );
+}
+
+const StyledWrapper = styled.div`
+ .loader {
+ position: relative;
+ width: 150px;
+ height: 150px;
+ border-radius: 50%;
+ perspective: 800px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ }
+
+ .inner {
+ position: absolute;
+ box-sizing: border-box;
+ width: 100%;
+ height: 100%;
+ border-radius: 50%;
+ }
+
+ /* Внешнее кольцо */
+ .inner.one {
+ left: 0%;
+ top: 0%;
+ animation: rotate-one 1.5s linear infinite;
+ border-bottom: 4px solid var(--accent-color);
+ box-shadow: 0 0 10px var(--accent-color);
+ filter: drop-shadow(0 0 5px var(--accent-color));
+ }
+
+ /* Среднее кольцо */
+ .inner.two {
+ right: 0%;
+ top: 0%;
+ animation: rotate-two 1.5s linear infinite;
+ border-right: 4px solid var(--text-color);
+ box-shadow: 0 0 10px var(--text-color);
+ opacity: 0.8;
+ }
+
+ /* Внутреннее кольцо */
+ .inner.three {
+ right: 0%;
+ bottom: 0%;
+ width: 70%;
+ height: 70%;
+ margin: 15%; /* Центрирование (100-70)/2 */
+ animation: rotate-three 1.5s linear infinite;
+ border-top: 4px solid #a855f7; /* Purple */
+ box-shadow: 0 0 10px #a855f7;
+ filter: drop-shadow(0 0 5px #a855f7);
+ }
+
+ /* Текст в центре */
+ .core-text {
+ font-family: monospace;
+ font-weight: 900;
+ font-size: 24px;
+ color: var(--text-color);
+ animation: pulse 2s infinite;
+ text-shadow: 0 0 10px var(--accent-color);
+ z-index: 10;
+ }
+
+ @keyframes rotate-one {
+ 0% { transform: rotateX(35deg) rotateY(-45deg) rotateZ(0deg); }
+ 100% { transform: rotateX(35deg) rotateY(-45deg) rotateZ(360deg); }
+ }
+
+ @keyframes rotate-two {
+ 0% { transform: rotateX(50deg) rotateY(10deg) rotateZ(0deg); }
+ 100% { transform: rotateX(50deg) rotateY(10deg) rotateZ(360deg); }
+ }
+
+ @keyframes rotate-three {
+ 0% { transform: rotateX(35deg) rotateY(55deg) rotateZ(0deg); }
+ 100% { transform: rotateX(35deg) rotateY(55deg) rotateZ(360deg); }
+ }
+
+ @keyframes pulse {
+ 0%, 100% { opacity: 1; transform: scale(1); }
+ 50% { opacity: 0.5; transform: scale(0.9); }
+ }
+`;
+
+export default OsCore;
\ No newline at end of file
diff --git a/src/components/Pogoda.jsx b/src/components/Pogoda.jsx
new file mode 100644
index 0000000..48364f3
--- /dev/null
+++ b/src/components/Pogoda.jsx
@@ -0,0 +1,161 @@
+import React from 'react';
+import styled from 'styled-components';
+
+const Pogoda = () => {
+ return (
+
+
+
+ {/* Верхняя часть: Основная инфо */}
+
+
+ {/* SVG Иконка: Облачно с прояснениями */}
+
+
+
+
+
+
+
18°C
+
Ангарск
+
Иркутская обл.
+
+
+
+ {/* Нижняя часть: Детали (Раскрывается при наведении) */}
+
+
+
+
+
+
+
+
+
+
+
+
752
+
мм рт.ст
+
+
+
+
+
+ );
+}
+
+const StyledWrapper = styled.div`
+ /* Стили контейнера */
+ .weather-card {
+ width: 200px;
+ height: 90px; /* Компактная высота по умолчанию */
+ background: rgba(255, 255, 255, 0.9);
+ border-radius: 20px;
+ padding: 15px;
+ box-shadow: 0 10px 30px rgba(0,0,0,0.2);
+ border: 1px solid rgba(255,255,255,0.5);
+ transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
+ overflow: hidden;
+ cursor: default;
+ display: flex;
+ flex-direction: column;
+ gap: 15px;
+ }
+
+ /* При наведении карточка растет */
+ .weather-card:hover {
+ height: 180px;
+ width: 220px;
+ transform: translateY(-10px);
+ background: #fff;
+ }
+
+ /* Верхняя часть */
+ .main-info {
+ display: flex;
+ align-items: center;
+ gap: 15px;
+ }
+
+ .weather-icon {
+ width: 50px;
+ height: 50px;
+ }
+
+ .text-container {
+ display: flex;
+ flex-direction: column;
+ }
+
+ .temp {
+ font-size: 24px;
+ font-weight: 800;
+ color: #1e293b;
+ line-height: 1;
+ }
+
+ .city {
+ font-size: 14px;
+ font-weight: 700;
+ color: #475569;
+ margin-top: 4px;
+ }
+
+ .desc {
+ font-size: 10px;
+ color: #94a3b8;
+ }
+
+ /* Детали (скрыты или сжаты по умолчанию) */
+ .details {
+ display: flex;
+ justify-content: space-between;
+ opacity: 0;
+ transform: translateY(20px);
+ transition: all 0.3s ease;
+ border-top: 1px solid #e2e8f0;
+ padding-top: 15px;
+ }
+
+ .weather-card:hover .details {
+ opacity: 1;
+ transform: translateY(0);
+ }
+
+ .detail-item {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 4px;
+ }
+
+ .detail-icon {
+ width: 20px;
+ height: 20px;
+ }
+
+ .detail-item span {
+ font-size: 12px;
+ font-weight: 700;
+ color: #334155;
+ }
+
+ .detail-item small {
+ font-size: 9px;
+ color: #64748b;
+ }
+`;
+
+export default Pogoda;
diff --git a/src/components/Radar.jsx b/src/components/Radar.jsx
new file mode 100644
index 0000000..5199f20
--- /dev/null
+++ b/src/components/Radar.jsx
@@ -0,0 +1,77 @@
+import React from 'react';
+import styled from 'styled-components';
+
+const Radar = () => {
+ return (
+
+
+
+
+
+ );
+}
+
+const StyledWrapper = styled.div`
+ .loader {
+ position: relative;
+ width: 120px;
+ height: 120px;
+ background: var(--glass-bg);
+ border-radius: 50%;
+ box-shadow: 0 0 50px var(--shadow-color);
+ border: 1px solid var(--text-color);
+ opacity: 0.8;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ overflow: hidden;
+ }
+ .loader::before {
+ content: '';
+ position: absolute;
+ inset: 20px;
+ background: transparent;
+ border: 1px dashed var(--text-color);
+ opacity: 0.3;
+ border-radius: 50%;
+ }
+ .loader::after {
+ content: '';
+ position: absolute;
+ width: 40px;
+ height: 40px;
+ border-radius: 50%;
+ border: 1px dashed var(--text-color);
+ opacity: 0.5;
+ }
+ .loader span {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ width: 50%;
+ height: 100%;
+ background: transparent;
+ transform-origin: top left;
+ animation: radar81 2s linear infinite;
+ border-top: 1px dashed var(--text-color);
+ }
+ .loader span::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: var(--accent-color);
+ transform-origin: top left;
+ transform: rotate(-55deg);
+ filter: blur(30px) drop-shadow(20px 20px 20px var(--accent-color));
+ opacity: 0.6;
+ }
+ @keyframes radar81 {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+ }
+`;
+
+export default Radar;
diff --git a/src/components/Record.jsx b/src/components/Record.jsx
new file mode 100644
index 0000000..27e9ae8
--- /dev/null
+++ b/src/components/Record.jsx
@@ -0,0 +1,102 @@
+import React from 'react';
+import styled from 'styled-components';
+
+const Button = () => {
+ return (
+
+
+
+ );
+}
+
+const StyledWrapper = styled.div`
+ .btn-wrapper {
+ /* Масштабируем кнопку, чтобы она была компактной */
+ transform: scale(0.7);
+ transform-origin: center right;
+
+ --width: 120px;
+ --height: 50px;
+ --padding: 4px;
+ --border-radius: 30px;
+ --dot-size: 10px;
+ --btn-color: #202020;
+ --hue: 355deg;
+ --animation-duration: 1.2s;
+
+ position: relative;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: var(--width);
+ height: var(--height);
+ border-radius: var(--border-radius);
+ border: none;
+ background-color: rgba(0,0,0,0.1);
+ box-shadow: 1px 1px 2px 0 rgba(255,255,255,0.1), 2px 2px 2px rgba(0,0,0,0.1) inset;
+ user-select: none;
+ z-index: 1;
+ }
+
+ .btn {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ padding: 0;
+ width: calc(100% - 2 * var(--padding));
+ height: calc(100% - 2 * var(--padding));
+ border-radius: calc(var(--border-radius) - var(--padding));
+ border: none;
+ cursor: pointer;
+ background: linear-gradient(rgba(255,255,255,0.1), rgba(0,0,0,0.1)), var(--btn-color);
+ box-shadow: 0 4px 8px rgba(0,0,0,0.3);
+ transition: all 0.2s ease;
+ z-index: 2;
+ }
+
+ .btn-txt {
+ font-size: 14px;
+ font-weight: 800;
+ font-family: monospace;
+ letter-spacing: 1px;
+ color: #fff;
+ text-transform: uppercase;
+ }
+
+ .dot {
+ position: relative;
+ width: var(--dot-size);
+ height: var(--dot-size);
+ border-radius: 50%;
+ background-color: #ff0000;
+ box-shadow: 0 0 10px #ff0000;
+ }
+
+ /* Анимация пульсации */
+ .pulse {
+ animation: pulse-red 1.5s infinite;
+ }
+
+ @keyframes pulse-red {
+ 0% { transform: scale(0.95); box-shadow: 0 0 0 0 rgba(255, 0, 0, 0.7); }
+ 70% { transform: scale(1); box-shadow: 0 0 0 6px rgba(255, 0, 0, 0); }
+ 100% { transform: scale(0.95); box-shadow: 0 0 0 0 rgba(255, 0, 0, 0); }
+ }
+
+ .btn:hover {
+ transform: translateY(-1px);
+ filter: brightness(1.2);
+ }
+ .btn:active {
+ transform: translateY(1px);
+ filter: brightness(0.9);
+ }
+`;
+
+export default Button;
diff --git a/src/components/Server.jsx b/src/components/Server.jsx
new file mode 100644
index 0000000..05b5997
--- /dev/null
+++ b/src/components/Server.jsx
@@ -0,0 +1,171 @@
+import React from 'react';
+import styled from 'styled-components';
+
+const Server = () => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+const StyledWrapper = styled.div`
+ .container_SevMini {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ flex-direction: column;
+ }
+
+ .Ghost {
+ transform: translate(0px, -25px);
+ z-index: -1;
+ animation: opacidad 4s infinite ease-in-out;
+ }
+
+ @keyframes opacidad {
+ 0% {
+ opacity: 1;
+ scale: 1;
+ }
+
+ 50% {
+ opacity: 0.5;
+ scale: 0.9;
+ }
+
+ 100% {
+ opacity: 1;
+ scale: 1;
+ }
+ }
+
+ @keyframes estroboscopico {
+ 0% {
+ opacity: 1;
+ }
+
+ 50% {
+ opacity: 0;
+ }
+
+ 51% {
+ opacity: 1;
+ }
+
+ 100% {
+ opacity: 1;
+ }
+ }
+
+ @keyframes rebote {
+ 0%,
+ 100% {
+ transform: translateY(0);
+ }
+
+ 50% {
+ transform: translateY(-10px);
+ }
+ }
+
+ @keyframes estroboscopico1 {
+ 0%,
+ 50%,
+ 100% {
+ fill: rgb(255, 95, 74);
+ }
+
+ 25%,
+ 75% {
+ fill: rgb(16, 53, 115);
+ }
+ }
+
+ @keyframes estroboscopico2 {
+ 0%,
+ 50%,
+ 100% {
+ fill: #17e300;
+ }
+
+ 25%,
+ 75% {
+ fill: #17e300b4;
+ }
+ }
+
+ .SevMini {
+ animation: rebote 4s infinite ease-in-out;
+ }
+
+ #strobe_led1 {
+ animation: estroboscopico 0.5s infinite;
+ }
+
+ #strobe_color1 {
+ animation: estroboscopico2 0.8s infinite;
+ }
+
+ #strobe_color3 {
+ animation: estroboscopico1 0.8s infinite;
+ animation-delay: 3s;
+ }`;
+
+export default Server;
diff --git a/src/components/Social.jsx b/src/components/Social.jsx
new file mode 100644
index 0000000..df4ab6c
--- /dev/null
+++ b/src/components/Social.jsx
@@ -0,0 +1,215 @@
+import React from 'react';
+import styled from 'styled-components';
+
+const Social = () => {
+ return (
+
+
+
+
Социальные сети
+
+
+
+ );
+}
+
+const StyledWrapper = styled.div`
+ /* CSS Variables for colors */
+
+ .card {
+ position: relative;
+ /* Уменьшил размеры */
+ width: 14em;
+ height: 18em;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ /* Адаптивный фон и цвет текста */
+ background-color: var(--card-bg);
+ color: var(--text-color);
+ border: 1px solid var(--glass-border);
+ font-family: Montserrat, sans-serif;
+ font-weight: bold;
+ padding: 1em;
+ border-radius: 20px;
+ overflow: hidden;
+ z-index: 1;
+ row-gap: 0.8em;
+ }
+
+ .card img {
+ /* Уменьшил картинку */
+ width: 8em;
+ margin-right: 0.5em;
+ animation: move 10s ease-in-out infinite;
+ z-index: 5;
+ }
+
+ .icons svg {
+ width: 18px;
+ height: 18px;
+ }
+
+ /* Гравитация и фон (тут используем переменные для теней) */
+ .card::before {
+ content: "";
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ inset: -3px;
+ border-radius: 10px;
+ background: radial-gradient(var(--text-color), transparent, transparent);
+ opacity: 0.1;
+ transform: translate(-5px, 250px);
+ transition: 0.4s ease-in-out;
+ z-index: -1;
+ }
+ .card:hover::before {
+ width: 150%;
+ height: 100%;
+ margin-left: -4.25em;
+ opacity: 0.1;
+ }
+ .card::after {
+ content: "";
+ position: absolute;
+ inset: 2px;
+ border-radius: 20px;
+ background: var(--card-bg);
+ opacity: 0.9;
+ transition: all 0.4s ease-in-out;
+ z-index: -1;
+ }
+
+ .heading {
+ z-index: 2;
+ transition: 0.4s ease-in-out;
+ font-size: 0.9em;
+ text-align: center;
+ }
+
+ /* ЗВЕЗДЫ (ТЕНИ) - заменяем #fff на var(--text-color) */
+ .heading::before {
+ content: "";
+ position: absolute;
+ top: 0; left: 0; width: 2px; height: 2px;
+ border-radius: 50%;
+ opacity: 0.5;
+ /* Генерируем "звезды" используя цвет текста (белый в темной теме, черный в светлой) */
+ box-shadow:
+ 220px 118px var(--text-color), 280px 176px var(--text-color), 40px 50px var(--text-color),
+ 60px 180px var(--text-color), 120px 130px var(--text-color), 180px 176px var(--text-color),
+ 220px 290px var(--text-color), 520px 250px var(--text-color), 400px 220px var(--text-color),
+ 50px 350px var(--text-color), 10px 230px var(--text-color);
+ z-index: -1;
+ transition: 1s ease;
+ animation: 1s glowing-stars linear alternate infinite;
+ }
+
+ /* Другие слои звезд */
+ .icons::before {
+ content: ""; position: absolute; top: 0; left: 0; width: 2px; height: 2px;
+ border-radius: 50%; opacity: 0.5;
+ box-shadow:
+ 140px 20px var(--text-color), 425px 20px var(--text-color), 70px 120px var(--text-color),
+ 20px 130px var(--text-color), 110px 80px var(--text-color), 280px 80px var(--text-color);
+ z-index: -1;
+ transition: 1.5s ease;
+ animation: 1s glowing-stars linear alternate infinite;
+ animation-delay: 0.4s;
+ }
+
+ /* Анимации и эффекты при наведении и нажатии */
+ .card:hover .heading::before,
+ .card:hover .icons::before {
+ filter: blur(3px);
+ }
+
+ .heading::after {
+ content: "";
+ top: -8.5%; left: -8.5%; position: absolute;
+ width: 7.5em; height: 7.5em;
+ border-radius: 50%;
+ background: var(--bg-color);
+ box-shadow: 0px 0px 100px var(--accent-color), inset var(--accent-color) 0px 0px 40px -12px;
+ opacity: 0.2;
+ transition: 0.4s ease-in-out;
+ z-index: -1;
+ }
+ .card:hover .heading::after {
+ box-shadow: 0px 0px 200px var(--accent-color), inset var(--accent-color) 0px 0px 40px -12px;
+ opacity: 0.6;
+ }
+
+ .icons {
+ display: flex; align-items: center; justify-content: center;
+ flex-direction: row; column-gap: 1em; z-index: 1;
+ }
+
+ .instagram, .x, .discord {
+ position: relative; transition: 0.4s ease-in-out;
+ color: var(--text-color);
+ }
+
+ .instagram:after, .x:after, .discord:after {
+ content: ""; position: absolute; width: 0.5em; height: 0.5em; left: 0;
+ background-color: var(--text-color);
+ box-shadow: 0px 0px 10px var(--shadow-color);
+ border-radius: 50%; z-index: -1; transition: 0.3s ease-in-out;
+ }
+
+ .instagram svg path, .x svg path, .discord svg path {
+ stroke: var(--text-color); opacity: 0.7; transition: 0.4s ease-in-out;
+ }
+ .instagram:hover svg path { stroke: #cc39a4; opacity: 1; }
+ .x:hover svg path { stroke: var(--text-color); opacity: 1; }
+ .discord:hover svg path { stroke: #8c9eff; opacity: 1; }
+
+ .instagram:hover svg { scale: 1.4; }
+ .x:hover svg, .discord:hover svg { scale: 1.25; }
+
+ .instagram:hover:after, .x:hover:after, .discord:hover:after {
+ scale: 4; transform: translateX(0.09em) translateY(0.09em);
+ }
+
+ /* Shooting stars logic changed to adapt variables */
+ @keyframes shootingStar {
+ 0% { transform: translateX(0) translateY(0); opacity: 1; }
+ 50% { transform: translateX(-55em) translateY(0); opacity: 1; }
+ 70% { transform: translateX(-70em) translateY(0); opacity: 0; }
+ 100% { transform: translateX(0) translateY(0); opacity: 0; }
+ }
+
+ @keyframes move {
+ 0% { transform: translateX(0em) translateY(0em); }
+ 25% { transform: translateY(-1em) translateX(-1em); rotate: -10deg; }
+ 50% { transform: translateY(1em) translateX(-1em); }
+ 75% { transform: translateY(-1.25em) translateX(1em); rotate: 10deg; }
+ 100% { transform: translateX(0em) translateY(0em); }
+ }
+
+ @keyframes glowing-stars {
+ 0% { opacity: 0; }
+ 50% { opacity: 1; }
+ 100% { opacity: 0; }
+ }
+`;
+
+export default Social;
diff --git a/src/components/Status.jsx b/src/components/Status.jsx
new file mode 100644
index 0000000..39c977f
--- /dev/null
+++ b/src/components/Status.jsx
@@ -0,0 +1,89 @@
+import React from 'react';
+import styled from 'styled-components';
+
+const Status = ({ status, date }) => {
+ // Определяем активный шаг на основе статуса
+ const getStatusIndex = (s) => {
+ if (s === 'new') return 1;
+ if (s === 'processing') return 2;
+ if (s === 'shipping') return 3;
+ if (s === 'completed') return 4;
+ return 0;
+ };
+
+ const currentStep = getStatusIndex(status || 'new');
+
+ return (
+
+
+ {/* STEP 1: PLACED */}
+
1 ? 'stepper-completed' : currentStep === 1 ? 'stepper-active' : 'stepper-pending'}`}>
+
{currentStep > 1 ? '✓' : '1'}
+
+
+
Размещен
+
{currentStep > 1 ? 'Готово' : currentStep === 1 ? 'Сейчас' : 'Ожидание'}
+
{date || '---'}
+
+
+
+ {/* STEP 2: PROCESSING */}
+
2 ? 'stepper-completed' : currentStep === 2 ? 'stepper-active' : 'stepper-pending'}`}>
+
{currentStep > 2 ? '✓' : '2'}
+
+
+
В обработке
+
{currentStep > 2 ? 'Готово' : currentStep === 2 ? 'В работе' : 'Ожидание'}
+
+
+
+ {/* STEP 3: SHIPPING */}
+
3 ? 'stepper-completed' : currentStep === 3 ? 'stepper-active' : 'stepper-pending'}`}>
+
{currentStep > 3 ? '✓' : '3'}
+
+
Доставка / Установка
+
{currentStep > 3 ? 'Готово' : currentStep === 3 ? 'В пути' : 'Ожидание'}
+
+
+
+
+ );
+}
+
+const StyledWrapper = styled.div`
+ .stepper-box {
+ background-color: var(--card-bg, #1e293b);
+ border: 1px solid var(--glass-border, rgba(255,255,255,0.1));
+ border-radius: 12px;
+ padding: 20px;
+ width: 100%;
+ color: var(--text-color, #fff);
+ }
+
+ .stepper-step { display: flex; margin-bottom: 20px; position: relative; }
+ .stepper-step:last-child { margin-bottom: 0; }
+
+ .stepper-line {
+ position: absolute; left: 15px; top: 35px; bottom: -25px; width: 2px;
+ background-color: rgba(255,255,255,0.1); z-index: 1;
+ }
+ .stepper-step:last-child .stepper-line { display: none; }
+
+ .stepper-circle {
+ width: 32px; height: 32px; border-radius: 50%;
+ display: flex; align-items: center; justify-content: center;
+ margin-right: 16px; z-index: 2; font-weight: bold; font-size: 14px;
+ transition: all 0.3s;
+ }
+
+ .stepper-completed .stepper-circle { background-color: #22c55e; color: white; }
+ .stepper-active .stepper-circle { border: 2px solid #3b82f6; color: #3b82f6; background: rgba(59, 130, 246, 0.1); }
+ .stepper-pending .stepper-circle { border: 2px solid #64748b; color: #64748b; }
+
+ .stepper-content { flex: 1; }
+ .stepper-title { font-weight: 600; font-size: 14px; margin-bottom: 2px; }
+ .stepper-status { font-size: 12px; opacity: 0.7; }
+ .stepper-time { font-size: 11px; opacity: 0.5; margin-top: 2px; }
+`;
+
+export default Status;
diff --git a/src/components/SupportChat.jsx b/src/components/SupportChat.jsx
new file mode 100644
index 0000000..1aba846
--- /dev/null
+++ b/src/components/SupportChat.jsx
@@ -0,0 +1,295 @@
+import React, { useState, useEffect, useRef } from 'react';
+import axios from 'axios';
+import { Send, X, MessageSquare, User, Shield, RefreshCw, Check, CheckCheck } from 'lucide-react';
+import { motion, AnimatePresence } from 'framer-motion';
+
+const API_URL = 'https://diplomnexus.aptcloud.ru';
+
+const SupportChat = ({ isOpen, onClose, user }) => {
+ const [messages, setMessages] = useState([]);
+ const [newMessage, setNewMessage] = useState('');
+
+ // Для админа - список пользователей, написавших сообщения
+ const [uniqueSenders, setUniqueSenders] = useState([]);
+ const [selectedEmail, setSelectedEmail] = useState(null); // Фильтр для админа (выбранный чат)
+ const [loading, setLoading] = useState(false);
+
+ // Проверка прав админа (по роли или имени)
+ const isAdmin = user?.role === 'admin' || user?.name === 'seth1nk' || user?.name === 'SuperAdmin';
+
+ const messagesEndRef = useRef(null);
+
+ // --- ЗАГРУЗКА СООБЩЕНИЙ ---
+ const fetchMessages = async () => {
+ try {
+ const token = localStorage.getItem('token');
+
+ // Если админ - загружаем все сообщения
+ if (isAdmin) {
+ const res = await axios.get(`${API_URL}/messages`, {
+ headers: { Authorization: `Bearer ${token}` }
+ });
+ const allMsgs = res.data;
+ setMessages(allMsgs);
+
+ // Извлекаем уникальных отправителей для списка контактов
+ // (фильтруем тех, кто писал, исключая админские ответы)
+ const senders = [];
+ const seen = new Set();
+
+ allMsgs.forEach(m => {
+ // Если сообщение от юзера (не админ) и мы его еще не видели
+ if (!m.is_admin && m.email && !seen.has(m.email)) {
+ seen.add(m.email);
+ senders.push({ name: m.user_name, email: m.email });
+ }
+ });
+ setUniqueSenders(senders);
+ }
+ // Если обычный юзер - загружаем только его сообщения (фильтр на сервере по email)
+ else {
+ const res = await axios.get(`${API_URL}/messages?email=${user.email}`, {
+ headers: { Authorization: `Bearer ${token}` }
+ });
+ setMessages(res.data);
+
+ // Если чат открыт, помечаем сообщения от админа как прочитанные
+ if (isOpen) {
+ await axios.post(`${API_URL}/messages/read`, { email: user.email }, {
+ headers: { Authorization: `Bearer ${token}` }
+ });
+ }
+ }
+ } catch (e) {
+ console.error("Ошибка загрузки сообщений:", e);
+ }
+ };
+
+ // Автообновление сообщений каждые 3 секунды
+ useEffect(() => {
+ if (isOpen) {
+ fetchMessages();
+ const interval = setInterval(fetchMessages, 3000);
+ return () => clearInterval(interval);
+ }
+ }, [isOpen, isAdmin, user]);
+
+ // Скролл вниз при новом сообщении
+ useEffect(() => {
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
+ }, [messages, selectedEmail, isOpen]);
+
+ // --- ОТПРАВКА СООБЩЕНИЯ ---
+ const handleSendMessage = async (e) => {
+ e.preventDefault();
+ if (!newMessage.trim()) return;
+
+ setLoading(true);
+ try {
+ // Если пишет АДМИН -> используем специальный эндпоинт ответа (эмуляция ответа бота)
+ // Или используем /contact, но сервер должен понять, что это админ.
+ // В текущей реализации сервера /contact всегда ставит is_admin = FALSE.
+ // Поэтому для админа лучше использовать /api/bot/reply (как будто бот ответил),
+ // ЧТОБЫ СОХРАНИЛОСЬ КАК is_admin = TRUE.
+
+ if (isAdmin && selectedEmail) {
+ await axios.post(`${API_URL}/api/bot/reply`, {
+ email: selectedEmail,
+ text: newMessage
+ });
+ } else {
+ // Если пишет ЮЗЕР
+ await axios.post(`${API_URL}/contact`, {
+ name: user.name,
+ email: user.email,
+ message: newMessage
+ });
+ }
+
+ setNewMessage('');
+ fetchMessages(); // Обновляем список сразу
+ } catch (error) {
+ console.error('Ошибка отправки:', error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ // --- ФИЛЬТРАЦИЯ СООБЩЕНИЙ ДЛЯ ОТОБРАЖЕНИЯ ---
+ // Если админ: показываем только сообщения выбранного юзера (и ответы ему)
+ // Если юзер: показываем все загруженные (они уже отфильтрованы сервером)
+ const displayedMessages = isAdmin
+ ? (selectedEmail ? messages.filter(m => m.email === selectedEmail) : [])
+ : messages;
+
+ return (
+
+ {isOpen && (
+
+ {/* --- HEADER --- */}
+
+
+
+
+ {isAdmin ? 'ПАНЕЛЬ ПОДДЕРЖКИ' : 'ЧАТ С ПОДДЕРЖКОЙ'}
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* --- BODY --- */}
+
+
+ {/* --- СПИСОК ЮЗЕРОВ (ТОЛЬКО ДЛЯ АДМИНА) --- */}
+ {isAdmin && !selectedEmail && (
+
+
Входящие обращения
+
+ {uniqueSenders.length === 0 && (
+
+ )}
+
+ {uniqueSenders.map((u) => (
+
setSelectedEmail(u.email)}
+ className="p-3 glass rounded-xl cursor-pointer hover:bg-[var(--accent-color)]/10 transition-colors flex items-center gap-3 border border-[var(--glass-border)] group"
+ >
+
+ {u.name ? u.name[0].toUpperCase() : '?'}
+
+
+
+ {u.name}
+ Открыть
+
+
{u.email}
+
+
+ ))}
+
+ )}
+
+ {/* --- ОКНО ЧАТА --- */}
+ {(!isAdmin || selectedEmail) && (
+
+
+ {/* Кнопка "Назад" для админа */}
+ {isAdmin && (
+
setSelectedEmail(null)}
+ className="text-xs text-[var(--text-color)] w-full py-2 bg-black/20 hover:bg-black/40 border-b border-[var(--glass-border)] flex items-center justify-center gap-2 transition-colors"
+ >
+ ← Назад к списку диалогов ({selectedEmail})
+
+ )}
+
+
+ {displayedMessages.length === 0 && (
+
+
+
+
+
Напишите нам!
+
Мы ответим в ближайшее время. История сохраняется.
+
+ )}
+
+ {displayedMessages.map((msg) => {
+ // ОПРЕДЕЛЯЕМ КТО ПИСАЛ
+ // is_admin=true -> Поддержка
+ // is_admin=false -> Юзер
+
+ // Если я Админ -> Мои сообщения это is_admin=true
+ // Если я Юзер -> Мои сообщения это is_admin=false
+ const isMe = isAdmin ? msg.is_admin : !msg.is_admin;
+
+ return (
+
+
+ {/* Имя отправителя (если не я) */}
+ {!isMe && (
+
+ {msg.is_admin ? 'Поддержка' : msg.user_name}
+
+ )}
+
+
{msg.text}
+
+ {/* Время и Галочки */}
+
+
+ {new Date(msg.created_at).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}
+
+
+ {/* Галочки показываем только для СВОИХ сообщений */}
+ {isMe && (
+ msg.is_read
+ ? // Прочитано (Синие)
+ : // Отправлено (Серые/Черные)
+ )}
+
+
+
+ );
+ })}
+
+
+
+ {/* --- INPUT --- */}
+
+
+ )}
+
+
+ )}
+
+ );
+};
+
+export default SupportChat;
diff --git a/src/components/Telephone.jsx b/src/components/Telephone.jsx
new file mode 100644
index 0000000..7171419
--- /dev/null
+++ b/src/components/Telephone.jsx
@@ -0,0 +1,136 @@
+import React from 'react';
+import styled from 'styled-components';
+
+const Telephone = () => {
+ return (
+
+
+
+ );
+}
+
+const StyledWrapper = styled.div`
+ /* Добавил стили для контейнера, чтобы уменьшить весь блок */
+ .container {
+ position: relative;
+ width: 100px; /* Ограничиваем место, которое занимает блок */
+ height: 100px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transform: scale(0.6); /* Уменьшаем визуально до 60% */
+ transform-origin: center center;
+ }
+
+ .loader {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ z-index: 10;
+ width: 160px;
+ height: 100px;
+ margin-left: -80px;
+ margin-top: -50px;
+ border-radius: 5px;
+ background: #1e3f57;
+ animation: dot1_ 3s cubic-bezier(0.55,0.3,0.24,0.99) infinite;
+ }
+
+ .loader:nth-child(2) {
+ z-index: 11;
+ width: 150px;
+ height: 90px;
+ margin-top: -45px;
+ margin-left: -75px;
+ border-radius: 3px;
+ background: #3c517d;
+ animation-name: dot2_;
+ }
+
+ .loader:nth-child(3) {
+ z-index: 12;
+ width: 40px;
+ height: 20px;
+ margin-top: 50px;
+ margin-left: -20px;
+ border-radius: 0 0 5px 5px;
+ background: #6bb2cd;
+ animation-name: dot3_;
+ }
+
+ @keyframes dot1_ {
+ 3%,97% {
+ width: 160px;
+ height: 100px;
+ margin-top: -50px;
+ margin-left: -80px;
+ }
+
+ 30%,36% {
+ width: 80px;
+ height: 120px;
+ margin-top: -60px;
+ margin-left: -40px;
+ }
+
+ 63%,69% {
+ width: 40px;
+ height: 80px;
+ margin-top: -40px;
+ margin-left: -20px;
+ }
+ }
+
+ @keyframes dot2_ {
+ 3%,97% {
+ height: 90px;
+ width: 150px;
+ margin-left: -75px;
+ margin-top: -45px;
+ }
+
+ 30%,36% {
+ width: 70px;
+ height: 96px;
+ margin-left: -35px;
+ margin-top: -48px;
+ }
+
+ 63%,69% {
+ width: 32px;
+ height: 60px;
+ margin-left: -16px;
+ margin-top: -30px;
+ }
+ }
+
+ @keyframes dot3_ {
+ 3%,97% {
+ height: 20px;
+ width: 40px;
+ margin-left: -20px;
+ margin-top: 50px;
+ }
+
+ 30%,36% {
+ width: 8px;
+ height: 8px;
+ margin-left: -5px;
+ margin-top: 49px;
+ border-radius: 8px;
+ }
+
+ 63%,69% {
+ width: 16px;
+ height: 4px;
+ margin-left: -8px;
+ margin-top: -37px;
+ border-radius: 10px;
+ }
+ }`;
+
+export default Telephone;
diff --git a/src/components/UserAvatar.jsx b/src/components/UserAvatar.jsx
new file mode 100644
index 0000000..eac5fea
--- /dev/null
+++ b/src/components/UserAvatar.jsx
@@ -0,0 +1,29 @@
+import React from 'react';
+import { User } from 'lucide-react';
+
+const UserAvatar = ({ user, className }) => {
+ // 1. Если есть фото от Google — показываем его (круглое)
+ if (user.picture) {
+ return (
+
+ );
+ }
+
+ // 2. Если фото нет — показываем простую заглушку (как ты просил)
+ return (
+
+ {/* Иконка человека, залитая цветом (fill) */}
+
+
+ );
+};
+
+export default UserAvatar;
\ No newline at end of file
diff --git a/src/components/google.jsx b/src/components/google.jsx
new file mode 100644
index 0000000..39d9de0
--- /dev/null
+++ b/src/components/google.jsx
@@ -0,0 +1,53 @@
+import React from 'react';
+import styled from 'styled-components';
+
+const GoogleButton = ({ onClick }) => {
+ return (
+
+
+
+
+
+
+
+
+ Continue with Google
+
+
+ );
+}
+
+const StyledWrapper = styled.div`
+ width: 100%;
+
+ .button {
+ width: 100%;
+ max-width: 100%;
+ display: flex;
+ padding: 0.5rem 1.4rem;
+ font-size: 0.875rem;
+ line-height: 1.25rem;
+ font-weight: 700;
+ text-align: center;
+ text-transform: uppercase;
+ vertical-align: middle;
+ align-items: center;
+ justify-content: center;
+ border-radius: 0.5rem;
+ border: 1px solid rgba(0, 0, 0, 0.25);
+ gap: 0.75rem;
+ color: rgb(65, 63, 63);
+ background-color: #fff;
+ cursor: pointer;
+ transition: all .6s ease;
+ }
+
+ .button svg {
+ height: 24px;
+ }
+
+ .button:hover {
+ transform: scale(1.02);
+ }`;
+
+export default GoogleButton;
diff --git a/src/components/lightdark.jsx b/src/components/lightdark.jsx
new file mode 100644
index 0000000..b17ae6d
--- /dev/null
+++ b/src/components/lightdark.jsx
@@ -0,0 +1,125 @@
+import React from 'react';
+import styled from 'styled-components';
+
+const LightDark = ({ toggleTheme, isLight }) => {
+ return (
+
+
+
+
+
+ {/* Звезды и прочее (оставил как было для краткости, код большой, но рабочий) */}
+
+
+
+
+
+
+
+
+ );
+}
+
+const StyledWrapper = styled.div`
+ /* УВЕЛИЧИЛ МАСШТАБ */
+ transform: scale(0.6);
+ transform-origin: center;
+
+ .bb8-toggle {
+ --toggle-size: 16px;
+ --toggle-width: 10.625em;
+ --toggle-height: 5.625em;
+ --toggle-offset: calc((var(--toggle-height) - var(--bb8-diameter)) / 2);
+ --toggle-bg: linear-gradient(#2c4770, #070e2b 35%, #628cac 50% 70%, #a6c5d4) no-repeat;
+ --bb8-diameter: 4.375em;
+ --radius: 99em;
+ --transition: 0.4s;
+ --accent: #de7d2f;
+ --bb8-bg: #fff;
+ }
+
+ /* ... (Остальной CSS код с Uiverse без изменений, он верный) ... */
+ /* Скопируй весь CSS из твоего сообщения сюда, он большой, но я его проверил - рабочий */
+ .bb8-toggle, .bb8-toggle *, .bb8-toggle *::before, .bb8-toggle *::after { box-sizing: border-box; }
+ .bb8-toggle { cursor: pointer; margin-top: var(--margin-top-for-head); font-size: var(--toggle-size); }
+ .bb8-toggle__checkbox { appearance: none; display: none; }
+ .bb8-toggle__container { width: var(--toggle-width); height: var(--toggle-height); background: var(--toggle-bg); background-size: 100% 11.25em; background-position-y: -5.625em; border-radius: var(--radius); position: relative; transition: var(--transition); }
+ .bb8 { display: flex; flex-direction: column; align-items: center; position: absolute; top: calc(var(--toggle-offset) - 1.688em + 0.188em); left: var(--toggle-offset); transition: var(--transition); z-index: 2; }
+ .bb8__head-container { position: relative; transition: var(--transition); z-index: 2; transform-origin: 1.25em 3.75em; }
+ .bb8__head { overflow: hidden; margin-bottom: -0.188em; width: 2.5em; height: 1.688em; background: linear-gradient(transparent 0.063em, dimgray 0.063em 0.313em, transparent 0.313em 0.375em, var(--accent) 0.375em 0.5em, transparent 0.5em 1.313em, silver 1.313em 1.438em, transparent 1.438em), linear-gradient(45deg, transparent 0.188em, var(--bb8-bg) 0.188em 1.25em, transparent 1.25em), linear-gradient(-45deg, transparent 0.188em, var(--bb8-bg) 0.188em 1.25em, transparent 1.25em), linear-gradient(var(--bb8-bg) 1.25em, transparent 1.25em); border-radius: var(--radius) var(--radius) 0 0; position: relative; z-index: 1; filter: drop-shadow(0 0.063em 0.125em gray); }
+ .bb8__head::before { content: ""; position: absolute; width: 0.563em; height: 0.563em; background: radial-gradient(0.125em circle at 0.25em 0.375em, red, transparent), radial-gradient(0.063em circle at 0.375em 0.188em, var(--bb8-bg) 50%, transparent 100%), linear-gradient(45deg, #000 0.188em, dimgray 0.313em 0.375em, #000 0.5em); border-radius: var(--radius); top: 0.413em; left: 50%; transform: translate(-50%); box-shadow: 0 0 0 0.089em lightgray, 0.563em 0.281em 0 -0.148em, 0.563em 0.281em 0 -0.1em var(--bb8-bg), 0.563em 0.281em 0 -0.063em; z-index: 1; transition: var(--transition); }
+ .bb8__head::after { content: ""; position: absolute; bottom: 0.375em; left: 0; width: 100%; height: 0.188em; background: linear-gradient(to right, var(--accent) 0.125em, transparent 0.125em 0.188em, var(--accent) 0.188em 0.313em, transparent 0.313em 0.375em, var(--accent) 0.375em 0.938em, transparent 0.938em 1em, var(--accent) 1em 1.125em, transparent 1.125em 1.875em, var(--accent) 1.875em 2em, transparent 2em 2.063em, var(--accent) 2.063em 2.25em, transparent 2.25em 2.313em, var(--accent) 2.313em 2.375em, transparent 2.375em 2.438em, var(--accent) 2.438em); transition: var(--transition); }
+ .bb8__antenna { position: absolute; transform: translateY(-90%); width: 0.059em; border-radius: var(--radius) var(--radius) 0 0; transition: var(--transition); }
+ .bb8__antenna:nth-child(1) { height: 0.938em; right: 0.938em; background: linear-gradient(#000 0.188em, silver 0.188em); }
+ .bb8__antenna:nth-child(2) { height: 0.375em; left: 50%; transform: translate(-50%, -90%); background: silver; }
+ .bb8__body { width: 4.375em; height: 4.375em; background: var(--bb8-bg); border-radius: var(--radius); position: relative; overflow: hidden; transition: var(--transition); z-index: 1; transform: rotate(45deg); background: linear-gradient(-90deg, var(--bb8-bg) 4%, var(--accent) 4% 10%, transparent 10% 90%, var(--accent) 90% 96%, var(--bb8-bg) 96%), linear-gradient(var(--bb8-bg) 4%, var(--accent) 4% 10%, transparent 10% 90%, var(--accent) 90% 96%, var(--bb8-bg) 96%), linear-gradient(to right, transparent 2.156em, silver 2.156em 2.219em, transparent 2.188em), linear-gradient(transparent 2.156em, silver 2.156em 2.219em, transparent 2.188em); background-color: var(--bb8-bg); }
+ .bb8__body::after { content: ""; bottom: 1.5em; left: 0.563em; position: absolute; width: 0.188em; height: 0.188em; background: rgb(236, 236, 236); color: rgb(236, 236, 236); border-radius: 50%; box-shadow: 0.875em 0.938em, 0 -1.25em, 0.875em -2.125em, 2.125em -2.125em, 3.063em -1.25em, 3.063em 0, 2.125em 0.938em; }
+ .bb8__body::before { content: ""; width: 2.625em; height: 2.625em; position: absolute; border-radius: 50%; z-index: 0.1; overflow: hidden; top: 50%; left: 50%; transform: translate(-50%, -50%); border: 0.313em solid var(--accent); background: radial-gradient(1em circle at center, rgb(236, 236, 236) 50%, transparent 51%), radial-gradient(1.25em circle at center, var(--bb8-bg) 50%, transparent 51%), linear-gradient(-90deg, transparent 42%, var(--accent) 42% 58%, transparent 58%), linear-gradient(var(--bb8-bg) 42%, var(--accent) 42% 58%, var(--bb8-bg) 58%); }
+ .artificial__hidden { position: absolute; border-radius: inherit; inset: 0; pointer-events: none; overflow: hidden; }
+ .bb8__shadow { content: ""; width: var(--bb8-diameter); height: 20%; border-radius: 50%; background: #3a271c; box-shadow: 0.313em 0 3.125em #3a271c; opacity: 0.25; position: absolute; bottom: 0; left: calc(var(--toggle-offset) - 0.938em); transition: var(--transition); transform: skew(-70deg); z-index: 1; }
+ .bb8-toggle__scenery { width: 100%; height: 100%; pointer-events: none; overflow: hidden; position: relative; border-radius: inherit; }
+ .bb8-toggle__scenery::before { content: ""; position: absolute; width: 100%; height: 30%; bottom: 0; background: #b18d71; z-index: 1; }
+ .bb8-toggle__cloud { z-index: 1; position: absolute; border-radius: 50%; }
+ .bb8-toggle__cloud:nth-last-child(1) { width: 0.875em; height: 0.625em; filter: blur(0.125em) drop-shadow(0.313em 0.313em #ffffffae) drop-shadow(-0.625em 0 #fff) drop-shadow(-0.938em -0.125em #fff); right: 1.875em; top: 2.813em; background: linear-gradient(to top right, #ffffffae, #ffffffae); transition: var(--transition); }
+ .bb8-toggle__cloud:nth-last-child(2) { top: 0.625em; right: 4.375em; width: 0.875em; height: 0.375em; background: #dfdedeae; filter: blur(0.125em) drop-shadow(-0.313em -0.188em #e0dfdfae) drop-shadow(-0.625em -0.188em #bbbbbbae) drop-shadow(-1em 0.063em #cfcfcfae); transition: 0.6s; }
+ .bb8-toggle__cloud:nth-last-child(3) { top: 1.25em; right: 0.938em; width: 0.875em; height: 0.375em; background: #ffffffae; filter: blur(0.125em) drop-shadow(0.438em 0.188em #ffffffae) drop-shadow(-0.625em 0.313em #ffffffae); transition: 0.8s; }
+ .gomrassen, .hermes, .chenini { position: absolute; border-radius: var(--radius); background: linear-gradient(#fff, #6e8ea2); top: 100%; }
+ .gomrassen { left: 0.938em; width: 1.875em; height: 1.875em; box-shadow: 0 0 0.188em #ffffff52, 0 0 0.188em #6e8ea24b; transition: var(--transition); }
+ .gomrassen::before, .gomrassen::after { content: ""; position: absolute; border-radius: inherit; box-shadow: inset 0 0 0.063em rgb(140, 162, 169); background: rgb(184, 196, 200); }
+ .gomrassen::before { left: 0.313em; top: 0.313em; width: 0.438em; height: 0.438em; } .gomrassen::after { width: 0.25em; height: 0.25em; left: 1.25em; top: 0.75em; }
+ .hermes { left: 3.438em; width: 0.625em; height: 0.625em; box-shadow: 0 0 0.125em #ffffff52, 0 0 0.125em #6e8ea24b; transition: 0.6s; }
+ .chenini { left: 4.375em; width: 0.5em; height: 0.5em; box-shadow: 0 0 0.125em #ffffff52, 0 0 0.125em #6e8ea24b; transition: 0.8s; }
+ .tatto-1, .tatto-2 { position: absolute; width: 1.25em; height: 1.25em; border-radius: var(--radius); }
+ .tatto-1 { background: #fefefe; right: 3.125em; top: 0.625em; box-shadow: 0 0 0.438em #fdf4e1; transition: var(--transition); }
+ .tatto-2 { background: linear-gradient(#e6ac5c, #d75449); right: 1.25em; top: 2.188em; box-shadow: 0 0 0.438em #e6ad5c3d, 0 0 0.438em #d755494f; transition: 0.7s; }
+ .bb8-toggle__star { position: absolute; width: 0.063em; height: 0.063em; background: #fff; border-radius: var(--radius); filter: drop-shadow(0 0 0.063em #fff); color: #fff; top: 100%; }
+ .bb8-toggle__star:nth-child(1) { left: 3.75em; box-shadow: 1.25em 0.938em, -1.25em 2.5em, 0 1.25em, 1.875em 0.625em, -3.125em 1.875em, 1.25em 2.813em; transition: 0.2s; }
+ .bb8-toggle__star:nth-child(2) { left: 4.688em; box-shadow: 0.625em 0, 0 0.625em, -0.625em -0.625em, 0.625em 0.938em, -3.125em 1.25em, 1.25em -1.563em; transition: 0.3s; }
+ .bb8-toggle__star:nth-child(3) { left: 5.313em; box-shadow: -0.625em -0.625em, -2.188em 1.25em, -2.188em 0, -3.75em -0.625em, -3.125em -0.625em, -2.5em -0.313em, 0.75em -0.625em; transition: var(--transition); }
+ .bb8-toggle__star:nth-child(4) { left: 1.875em; width: 0.125em; height: 0.125em; transition: 0.5s; }
+ .bb8-toggle__star:nth-child(5) { left: 5em; width: 0.125em; height: 0.125em; transition: 0.6s; }
+ .bb8-toggle__star:nth-child(6) { left: 2.5em; width: 0.125em; height: 0.125em; transition: 0.7s; }
+ .bb8-toggle__star:nth-child(7) { left: 3.438em; width: 0.125em; height: 0.125em; transition: 0.8s; }
+ .bb8-toggle__checkbox:checked + .bb8-toggle__container .bb8-toggle__star:nth-child(1) { top: 0.625em; }
+ .bb8-toggle__checkbox:checked + .bb8-toggle__container .bb8-toggle__star:nth-child(2) { top: 1.875em; }
+ .bb8-toggle__checkbox:checked + .bb8-toggle__container .bb8-toggle__star:nth-child(3) { top: 1.25em; }
+ .bb8-toggle__checkbox:checked + .bb8-toggle__container .bb8-toggle__star:nth-child(4) { top: 3.438em; }
+ .bb8-toggle__checkbox:checked + .bb8-toggle__container .bb8-toggle__star:nth-child(5) { top: 3.438em; }
+ .bb8-toggle__checkbox:checked + .bb8-toggle__container .bb8-toggle__star:nth-child(6) { top: 0.313em; }
+ .bb8-toggle__checkbox:checked + .bb8-toggle__container .bb8-toggle__star:nth-child(7) { top: 1.875em; }
+ .bb8-toggle__checkbox:checked + .bb8-toggle__container .bb8-toggle__cloud { right: -100%; }
+ .bb8-toggle__checkbox:checked + .bb8-toggle__container .gomrassen { top: 0.938em; }
+ .bb8-toggle__checkbox:checked + .bb8-toggle__container .hermes { top: 2.5em; }
+ .bb8-toggle__checkbox:checked + .bb8-toggle__container .chenini { top: 2.75em; }
+ .bb8-toggle__checkbox:checked + .bb8-toggle__container { background-position-y: 0; }
+ .bb8-toggle__checkbox:checked + .bb8-toggle__container .tatto-1 { top: 100%; }
+ .bb8-toggle__checkbox:checked + .bb8-toggle__container .tatto-2 { top: 100%; }
+ .bb8-toggle__checkbox:checked + .bb8-toggle__container .bb8 { left: calc(100% - var(--bb8-diameter) - var(--toggle-offset)); }
+ .bb8-toggle__checkbox:checked + .bb8-toggle__container .bb8__shadow { left: calc(100% - var(--bb8-diameter) - var(--toggle-offset) + 0.938em); transform: skew(70deg); }
+ .bb8-toggle__checkbox:checked + .bb8-toggle__container .bb8__body { transform: rotate(225deg); }
+ .bb8-toggle__checkbox:hover + .bb8-toggle__container .bb8__head::before { left: 100%; }
+ .bb8-toggle__checkbox:not(:checked):hover + .bb8-toggle__container .bb8__antenna:nth-child(1) { right: 1.5em; }
+ .bb8-toggle__checkbox:hover + .bb8-toggle__container .bb8__antenna:nth-child(2) { left: 0.938em; }
+ .bb8-toggle__checkbox:hover + .bb8-toggle__container .bb8__head::after { background-position: 1.375em 0; }
+ .bb8-toggle__checkbox:checked:hover + .bb8-toggle__container .bb8__head::before { left: 0; }
+ .bb8-toggle__checkbox:checked:hover + .bb8-toggle__container .bb8__antenna:nth-child(2) { left: calc(100% - 0.938em); }
+ .bb8-toggle__checkbox:checked:hover + .bb8-toggle__container .bb8__head::after { background-position: -1.375em 0; }
+ .bb8-toggle__checkbox:active + .bb8-toggle__container .bb8__head-container { transform: rotate(25deg); }
+ .bb8-toggle__checkbox:checked:active + .bb8-toggle__container .bb8__head-container { transform: rotate(-25deg); }
+`;
+
+export default LightDark;
diff --git a/src/index.css b/src/index.css
index 661641e..1567d75 100644
--- a/src/index.css
+++ b/src/index.css
@@ -1,40 +1,143 @@
-@tailwind base;
-@tailwind components;
-@tailwind utilities;
+@import "tailwindcss";
+@config "../tailwind.config.js";
+@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&display=swap');
+@import "./styles/admin.css";
+@import "./styles/pages.css";
:root {
- --bg-color: #060713;
- --card-bg: #0b0c15;
- --text-color: #ffffff;
- --accent-color: #00f260; /* Фирменный зеленый неон */
- --glass-border: rgba(255, 255, 255, 0.08);
- --input-bg: rgba(0, 0, 0, 0.4);
+ /* ТЕМНАЯ ТЕМА (Deep Space) */
+ --bg-color: #0b0c15;
+ --text-color: #f1f5f9;
+ --glass-bg: rgba(20, 20, 30, 0.6);
+ --glass-border: rgba(255, 255, 255, 0.05);
+ --accent-color: #00f3ff;
+ --card-bg: #13141f;
+ --shadow-color: rgba(0, 243, 255, 0.1);
+ --map-filter: grayscale(100%) invert(100%) contrast(1.2); /* Делаем карту темной */
+ --input-bg: rgba(15, 23, 42, 0.6);
+}
+
+body.light {
+ /* СВЕТЛАЯ ТЕМА (Tech Lab) */
+ --bg-color: #f0f2f5;
+ --text-color: #1a1c23;
+ --glass-bg: rgba(255, 255, 255, 0.8);
+ --glass-border: rgba(0, 0, 0, 0.05);
+ --accent-color: #2563eb;
+ --card-bg: #ffffff;
+ --input-bg: rgba(255, 255, 255, 0.9);
+ --shadow-color: rgba(37, 99, 235, 0.15);
+ --map-filter: grayscale(0%) invert(0%); /* Обычная карта */
}
body {
- background-color: var(--bg-color);
+ background: var(--bg-color);
color: var(--text-color);
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+ font-family: 'Inter', sans-serif;
+ transition: background 0.4s ease, color 0.4s ease;
overflow-x: hidden;
}
-.custom-scroll::-webkit-scrollbar {
- width: 4px;
-}
-.custom-scroll::-webkit-scrollbar-thumb {
- background: var(--accent-color);
- border-radius: 10px;
+/* Скроллбар */
+::-webkit-scrollbar { width: 6px; }
+::-webkit-scrollbar-track { background: transparent; }
+::-webkit-scrollbar-thumb { background: var(--accent-color); border-radius: 10px; }
+
+/* Эффект стекла (Без рамок, только тень и блюр) */
+.glass {
+ background: var(--glass-bg);
+ backdrop-filter: blur(16px);
+ -webkit-backdrop-filter: blur(16px);
+ border: 1px solid var(--glass-border);
+ box-shadow: 0 8px 32px 0 var(--shadow-color);
}
+/* Кнопка */
.btn-neon {
- background: rgba(0, 242, 96, 0.1);
- border: 1px solid var(--accent-color);
- color: var(--accent-color);
- box-shadow: 0 0 15px rgba(0, 242, 96, 0.2);
- transition: all 0.3s ease;
+ background: linear-gradient(135deg, var(--accent-color), #8b5cf6);
+ color: white;
+ font-weight: 700;
+ border-radius: 12px;
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+ box-shadow: 0 4px 15px var(--shadow-color);
+ position: relative;
+ overflow: hidden;
}
.btn-neon:hover {
- background: var(--accent-color);
- color: #000;
- box-shadow: 0 0 25px rgba(0, 242, 96, 0.4);
+ transform: translateY(-2px) scale(1.02);
+ box-shadow: 0 8px 25px var(--shadow-color);
+}
+
+/* --- СТИЛИ ДЛЯ КОМПОНЕНТОВ UIVERSE (Вставляем сюда, чтобы работали в Dashboard) --- */
+
+/* 1. NEON CARD */
+.uiverse-card {
+ width: 100%; height: 320px;
+ background: var(--card-bg);
+ position: relative; display: flex; place-content: center; place-items: center;
+ overflow: hidden; border-radius: 20px;
+ box-shadow: 0 10px 20px rgba(0,0,0,0.2);
+}
+.uiverse-card::before {
+ content: ''; position: absolute; width: 100px;
+ background-image: linear-gradient(180deg, var(--accent-color), #bc13fe);
+ height: 150%; animation: rotBGimg 4s linear infinite; transition: all 0.2s linear;
+}
+@keyframes rotBGimg { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
+.uiverse-card::after {
+ content: ''; position: absolute; background: var(--card-bg); inset: 3px; border-radius: 18px;
+}
+.uiverse-card-content {
+ position: absolute; z-index: 10; width: 92%; height: 92%;
+ display: flex; flex-direction: column; justify-content: space-between;
+}
+
+/* 2. CYBER SWITCH */
+.cyber-switch { font-size: 14px; position: relative; display: inline-block; width: 3.5em; height: 2em; }
+.cyber-switch input { opacity: 0; width: 0; height: 0; }
+.cyber-slider {
+ position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0;
+ background-color: #1a1a2e; transition: .4s; border-radius: 10px;
+ border: 1px solid var(--accent-color);
+}
+.cyber-slider:before {
+ position: absolute; content: ""; height: 1.4em; width: 1.4em; left: 0.3em; bottom: 0.25em;
+ background-color: var(--accent-color); transition: .4s; border-radius: 50%;
+ box-shadow: 0 0 10px var(--accent-color);
+}
+.cyber-switch input:checked + .cyber-slider { background-color: var(--accent-color); border-color: white; }
+.cyber-switch input:checked + .cyber-slider:before {
+ transform: translateX(1.5em); background-color: white; box-shadow: none;
+}
+/* Обновленный класс для инпутов */
+.contact-input {
+ width: 100%;
+ background-color: var(--input-bg); /* Используем переменную */
+ border: 1px solid var(--glass-border);
+ border-radius: 0.75rem;
+ padding: 1rem;
+ color: var(--text-color);
+ outline: none;
+ transition: all 0.3s ease;
+ font-size: 1rem;
+}
+
+.contact-input:focus {
+ border-color: var(--accent-color);
+ box-shadow: 0 0 0 4px rgba(var(--accent-color), 0.1); /* Свечение */
+ background-color: var(--card-bg);
+}
+
+/* Плейсхолдеры */
+.contact-input::placeholder {
+ color: var(--text-color);
+ opacity: 0.5;
+}
+@keyframes scan {
+ 0% { transform: translateY(-50%); }
+ 100% { transform: translateY(50%); }
+}
+
+.animate-scan {
+ animation: scan 2s linear infinite;
}
\ No newline at end of file
diff --git a/src/main.jsx b/src/main.jsx
index c6e6194..e065683 100644
--- a/src/main.jsx
+++ b/src/main.jsx
@@ -2,12 +2,12 @@ import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import './index.css'
-import bridge from '@vkontakte/vk-bridge'
+import bridge from '@vkontakte/vk-bridge' // Импортируем мост
-// Инициализируем VK Bridge до рендеринга приложения
+// Инициализируем мост ВК
bridge.send('VKWebAppInit')
.then(() => console.log('VK Bridge успешно инициализирован'))
- .catch((err) => console.error('Ошибка инициализации VK Bridge:', err));
+ .catch((err) => console.error('Ошибка моста:', err));
ReactDOM.createRoot(document.getElementById('root')).render(
diff --git a/src/pages/About.jsx b/src/pages/About.jsx
new file mode 100644
index 0000000..682b534
--- /dev/null
+++ b/src/pages/About.jsx
@@ -0,0 +1,205 @@
+import { motion } from 'framer-motion';
+import { Activity, Shield, Cpu, Wifi } from 'lucide-react';
+import Mesto from '../components/Mesto';
+import Pogoda from '../components/Pogoda';
+import Notebook from '../components/Notebook';
+import Server from '../components/Server';
+import Button from '../components/Record';
+const About = () => {
+ return (
+
+
+
+
+
+
+
+
+
+ {/* === ОСНОВНОЙ КОНТЕНТ === */}
+
+ {/* HEADER */}
+
+ О Проекте
+
+ Экосистема
+ Smart Nexus
+
+
+ Интеллектуальное управление пространством. Мы превращаем квадратные метры в думающий организм.
+
+
+
+ {/* TEXT & DASHBOARD BLOCK */}
+
+
+ {/* Text */}
+
+ Центральный нейро-хаб
+
+ В основе системы лежит локальный сервер обработки данных. В отличие от облачных решений,
+ Smart Nexus обрабатывает все сигналы внутри дома (Edge Computing), обеспечивая мгновенную реакцию
+ и полную безопасность.
+
+
+
+ {/* === БОЛЬШАЯ ПАНЕЛЬ МОНИТОРИНГА === */}
+
+ {/* Фон свечение */}
+
+
+ {/* ЛЕВАЯ ЧАСТЬ: ВИЗУАЛИЗАЦИЯ (СЕРВЕР) */}
+
+
+ UNIT: ALPHA-01
+
+
+
+
+
+
+
+ {/* ПРАВАЯ ЧАСТЬ: МЕТРИКИ (НОВОЕ) */}
+
+
+
+
+ Телеметрия Ядра
+
+
+ {/* Progress Bars */}
+
+ {/* CPU */}
+
+
+ {/* RAM */}
+
+
+ SECURITY LAYER
+ ACTIVE
+
+
+
+
+
+
+ {/* NETWORK */}
+
+
+ UPLINK
+ 1.2 Gbps
+
+ {/* График полосочками */}
+
+ {[40, 70, 30, 80, 50, 90, 60, 40, 70, 50, 80, 60].map((h, i) => (
+
+ ))}
+
+
+
+
+ {/* Info Grid */}
+
+
+
+
+
+
+
+ {/* ENGINEER TERMINAL SECTION */}
+
+
+
Инженерный доступ
+
+ Полный контроль над сценариями автоматизации. Доступ к логам системы, настройка чувствительности датчиков и обновление прошивок модулей.
+
+
+
v.2.4.1
+
connect --secure root@nexus
+
Authenticating...
+
Access Granted.
+
_
+
+
+
+
+
+
+ );
+};
+
+export default About;
\ No newline at end of file
diff --git a/src/pages/Contact.jsx b/src/pages/Contact.jsx
new file mode 100644
index 0000000..c157408
--- /dev/null
+++ b/src/pages/Contact.jsx
@@ -0,0 +1,171 @@
+import { motion } from 'framer-motion';
+import { MapPin, Phone, Mail, Send } from 'lucide-react';
+import Radar from '../components/Radar';
+import Social from '../components/Social';
+import Telephone from '../components/Telephone';
+
+const Contact = () => {
+ return (
+
+
+ {/* === ПЛАВАЮЩИЙ SOCIAL (FIXED) === */}
+
+ {/* pointer-events-auto нужен, чтобы клики работали, но контейнер не мешал */}
+
+
+
+
+
+ {/* ЗАГОЛОВОК */}
+
+
+ Центр
+ Связи
+
+
+ Ангарский политехнический техникум.
+ Техническая поддержка систем Smart Nexus.
+
+
+
+ {/* ГЛАВНАЯ СЕТКА */}
+
+
+ {/* === ЛЕВАЯ КОЛОНКА (Форма) === */}
+ {/* Добавлен flex и h-full, чтобы колонка тянулась вниз */}
+
+
+
+ Входящий сигнал
+
+
+
+
+ {/* === ПРАВАЯ КОЛОНКА (Карта + Контакты) === */}
+
+
+ {/* 1. КАРТА */}
+
+ {/* Плашка адреса */}
+
+
+
+
+
+ АПТ
+ 52.549955, 103.885752
+
+
+
+ {/* Радар */}
+
+
+
+
+ {/* Карта */}
+
+
+
+
+
+ {/* 2. БЛОК КОНТАКТОВ + ТЕЛЕФОН */}
+
+
+ {/* Текстовые данные: В ОДНУ СТРОКУ (Flex Row) */}
+
+
+ {/* Телефон */}
+
+
+
+ +7 (999) 000-NEXUS
+
+
+
+ {/* Вертикальный разделитель (только на десктопе) */}
+
+
+ {/* Почта */}
+
+
+
+ core@nexus.tech
+
+
+
+
+ {/* Анимация Телефона: УМЕНЬШЕНА И СДВИНУТА */}
+ {/* pr-8 md:pr-12 создает отступ от правого края блока */}
+
+
+ {/* Фон */}
+
+
+
+
+
+
+
+ );
+};
+
+export default Contact;
\ No newline at end of file
diff --git a/src/pages/Dashboard.jsx b/src/pages/Dashboard.jsx
new file mode 100644
index 0000000..01bba41
--- /dev/null
+++ b/src/pages/Dashboard.jsx
@@ -0,0 +1,255 @@
+import { useState, useEffect } from 'react';
+import axios from 'axios';
+import { ShoppingCart, History, Trash2, Plus, Minus, ArrowLeft, ArrowRight, Package, Cpu, ChevronLeft, ChevronRight } from 'lucide-react';
+import { useNavigate } from 'react-router-dom';
+import { motion, AnimatePresence } from 'framer-motion';
+import Status from '../components/Status';
+
+const API_URL = 'https://diplomnexus.aptcloud.ru';
+
+const categoryMap = [
+ { label: 'ВСЕ', value: 'Все' },
+ { label: 'ДАТЧИКИ', value: 'sensors' },
+ { label: 'КАМЕРЫ', value: 'cameras' },
+ { label: 'ОСВЕЩЕНИЕ', value: 'lighting' },
+ { label: 'ХАБЫ', value: 'hubs' }
+];
+
+const translateCategory = (cat) => {
+ const map = {
+ sensors: 'ДАТЧИКИ',
+ cameras: 'КАМЕРЫ',
+ lighting: 'ОСВЕЩЕНИЕ',
+ hubs: 'ХАБЫ'
+ };
+ return map[cat] || cat.toUpperCase();
+};
+
+const Dashboard = ({ user }) => {
+ const [items, setItems] = useState([]);
+ const [cart, setCart] = useState(() => JSON.parse(localStorage.getItem('cart') || '[]'));
+ const [orders, setOrders] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [selectedCat, setSelectedCat] = useState('Все');
+ const [currentPage, setCurrentPage] = useState(1);
+ const [isPaused, setIsPaused] = useState(false);
+ const [orderIndex, setOrderIndex] = useState(0);
+
+ const itemsPerPage = 9;
+ const navigate = useNavigate();
+
+ useEffect(() => {
+ const fetchData = async () => {
+ try {
+ const token = localStorage.getItem('token');
+ const [prodRes, orderRes] = await Promise.all([
+ axios.get(`${API_URL}/products`),
+ axios.get(`${API_URL}/orders`, { headers: { Authorization: `Bearer ${token}` } })
+ ]);
+ setItems(prodRes.data);
+ setOrders(orderRes.data);
+ } catch (e) { console.error(e); } finally { setLoading(false); }
+ };
+ fetchData();
+ }, []);
+
+ useEffect(() => {
+ localStorage.setItem('cart', JSON.stringify(cart));
+ }, [cart]);
+
+ useEffect(() => {
+ setCurrentPage(1);
+ }, [selectedCat]);
+
+ const getPaginationGroup = (totalPages) => {
+ let start = Math.max(1, currentPage - 5);
+ let end = Math.min(totalPages, start + 11);
+ if (end - start < 11) {
+ start = Math.max(1, end - 11);
+ }
+ const pages = [];
+ for (let i = start; i <= end; i++) {
+ pages.push(i);
+ }
+ return pages;
+ };
+
+ const addToCart = (item) => {
+ setCart(prev => {
+ const exist = prev.find(i => i.id === item.id && i.category === item.category);
+ if (exist) return prev.map(i => (i.id === item.id && i.category === item.category) ? {...i, qty: i.qty + 1} : i);
+ return [...prev, {...item, qty: 1}];
+ });
+ };
+
+ const removeFromCart = (id, category) => setCart(prev => prev.filter(i => !(i.id === id && i.category === category)));
+ const updateQty = (id, category, delta) => {
+ setCart(prev => prev.map(i => (i.id === id && i.category === category) ? { ...i, qty: Math.max(1, i.qty + delta) } : i));
+ };
+
+ const handlePayment = () => {
+ if(cart.length === 0) return;
+ localStorage.setItem('tempCart', JSON.stringify(cart));
+ navigate('/payment');
+ };
+
+ const filteredItems = selectedCat === 'Все' ? items : items.filter(i => i.category === selectedCat);
+ const totalPages = Math.ceil(filteredItems.length / itemsPerPage);
+ const currentItems = filteredItems.slice((currentPage - 1) * itemsPerPage, currentPage * itemsPerPage);
+
+ const visibleOrders = (() => {
+ if (orders.length === 0) return [];
+ if (orders.length < 5) return orders;
+ const res = [];
+ for (let i = 0; i < 5; i++) {
+ result.push(orders[(orderIndex + i) % orders.length]);
+ }
+ return res;
+ })();
+
+ if (loading) return LOADING_NEXUS_SYSTEM...
;
+
+ return (
+
+
+ {/* HEADER */}
+
+
+
+ ТЕРМИНАЛ NEXUS
+
+
:: OPERATOR: {user?.name} :: ONLINE
+
+
+ {/* ФИЛЬТРЫ КАТЕГОРИЙ (ИСПРАВЛЕНО: text-white) */}
+
+ {categoryMap.map(cat => (
+ setSelectedCat(cat.value)}
+ className={`px-6 py-2.5 rounded-xl font-bold text-[11px] uppercase tracking-widest transition-all border ${
+ selectedCat === cat.value
+ ? 'bg-[var(--accent-color)] text-white border-[var(--accent-color)] shadow-[0_0_20px_rgba(var(--accent-color),0.4)]'
+ : 'glass text-[var(--text-color)] hover:bg-white/5 border-[var(--glass-border)]'
+ }`}
+ >
+ {cat.label}
+
+ ))}
+
+
+
+
+
+
+
+ {currentItems.map((item) => (
+
+
+
+
+
{translateCategory(item.category)}
+
{item.price} ₽
+
+
+
{item.name}
+
{item.description}
+
+
addToCart(item)} className="mt-4 w-full py-3 rounded-xl bg-[var(--accent-color)]/10 text-[var(--accent-color)] font-bold text-xs tracking-[0.2em] border border-[var(--accent-color)]/20 hover:bg-[var(--accent-color)] hover:text-white transition-all">
+ {cart.some(c => c.id === item.id && c.category === item.category) ? 'В ХРАНИЛИЩЕ' : 'ИНТЕГРИРОВАТЬ'}
+
+
+
+ ))}
+
+
+
+ {/* ПАГИНАЦИЯ (ИСПРАВЛЕНО: text-white) */}
+ {totalPages > 1 && (
+
+
setCurrentPage(p => Math.max(1, p - 1))} disabled={currentPage === 1} className="p-3 glass rounded-xl hover:text-[var(--accent-color)] disabled:opacity-20">
+
+ {getPaginationGroup(totalPages).map((i) => (
+ setCurrentPage(i)}
+ className={`w-10 h-10 shrink-0 rounded-xl font-bold font-mono transition-all ${
+ currentPage === i
+ ? 'bg-[var(--accent-color)] text-white shadow-[0_0_15px_var(--accent-color)]'
+ : 'glass text-[var(--text-color)] opacity-50 hover:opacity-100'
+ }`}
+ >
+ {i}
+
+ ))}
+
+
setCurrentPage(p => Math.min(totalPages, p + 1))} disabled={currentPage === totalPages} className="p-3 glass rounded-xl hover:text-[var(--accent-color)] disabled:opacity-20">
+
+ )}
+
+
+
+
+
+ {/* НИЖНЯЯ КАРУСЕЛЬ */}
+ {orders.length > 0 && (
+
setIsPaused(true)} onMouseLeave={() => setIsPaused(false)}>
+
+
ЛОГ ОПЕРАЦИЙ
+
+ setOrderIndex(p => (p - 1 + orders.length) % orders.length)} className="p-3 rounded-xl glass hover:text-[var(--accent-color)]">
+ setOrderIndex(p => (p + 1) % orders.length)} className="p-3 rounded-xl glass hover:text-[var(--accent-color)]">
+
+
+
+
+ {visibleOrders.map((order, i) => (
+
+
+ #{order.order_number} {order.total}₽
+ {order.content}
+
+
+
+
+ ))}
+
+
+
+ )}
+
+ );
+};
+
+export default Dashboard;
diff --git a/src/pages/Home.jsx b/src/pages/Home.jsx
new file mode 100644
index 0000000..7733ef0
--- /dev/null
+++ b/src/pages/Home.jsx
@@ -0,0 +1,249 @@
+import { Link } from 'react-router-dom';
+import { ArrowRight, Shield, Zap, Smartphone, Users, Package, Activity, Cpu, Globe } from 'lucide-react';
+import { motion } from 'framer-motion';
+import { useState, useEffect } from 'react';
+import axios from 'axios';
+import ArcReactor from '../components/ArcReactor';
+import Iphone from '../components/Iphone';
+import Fingerprint from '../components/Fingerprint';
+import OsCore from '../components/OsCore'; // <--- НОВЫЙ ИМПОРТ
+
+const Home = () => {
+ const [stats, setStats] = useState({ users: 0, products: 0, orders: 0 });
+
+ useEffect(() => {
+ const fetchStats = async () => {
+ try {
+ const [u, p, o] = await Promise.all([
+ axios.get('https://diplomnexus.aptcloud.ru/users', { headers: { Authorization: `Bearer ${localStorage.getItem('token')}` }}),
+ axios.get('https://diplomnexus.aptcloud.ru/products'),
+ axios.get('https://diplomnexus.aptcloud.ru/orders', { headers: { Authorization: `Bearer ${localStorage.getItem('token')}` }})
+ ]);
+ setStats({ users: u.data.length, products: p.data.length, orders: o.data.length });
+ } catch (e) {
+ // Fallback данные для красоты, если бэк не отвечает
+ setStats({ users: 1240, products: 48, orders: 8900 });
+ }
+ };
+ fetchStats();
+ }, []);
+
+ // Анимация для контейнеров
+ const containerVariants = {
+ hidden: { opacity: 0 },
+ visible: { opacity: 1, transition: { staggerChildren: 0.2 } }
+ };
+
+ const itemVariants = {
+ hidden: { y: 20, opacity: 0 },
+ visible: { y: 0, opacity: 1 }
+ };
+
+ return (
+
+
+ {/* ФОНОВАЯ СЕТКА (Grid Background) */}
+
+
+ {/* === HERO SECTION === */}
+
+
+
+ {/* Левая часть: Текст */}
+
+
+
+
+
+
+ SYSTEM NEXUS V.5.0
+
+
+
+ УМНЫЙ
+ ДОМ
+
+
+
+ Мы не просто автоматизируем рутину. Мы создаем цифровую нервную систему вашего жилища, которая чувствует, думает и защищает.
+
+
+
+
+ ОТКРЫТЬ КАТАЛОГ
+
+
+
+
+ О СИСТЕМЕ
+
+
+
+
+ {/* Правая часть: Реактор */}
+
+ {/* Декоративные круги */}
+
+
+
+
+
+
+
+ {/* === STATS SECTION === */}
+
+
+
+ {/* Card 1 */}
+
+
+
{stats.users}
+
Пользователей
+
+
+
+
+
+
+ {/* Card 2 */}
+
+
+
{stats.products}
+
Модулей
+
+
+
+
+
+
+ {/* Card 3 */}
+
+
+
{stats.orders}+
+
Установок
+
+
+
+
+
+
+
+ {/* === SHOWCASE SECTION (Компоненты) === */}
+
+
+
+
+ ТЕХНОЛОГИЧЕСКИЙ СТЕК
+
+
+
+
+
+ {/* 1. MOBILE APP */}
+
+
+
+
+
+ Мобильный Контроль
+ Управление домом из любой точки мира. Полная синхронизация в реальном времени.
+
+
+ {/* 2. CORE SYSTEM (OS CORE) - CENTER */}
+
+ {/* Glowing bg */}
+
+
+
+ {/* НОВЫЙ КОМПОНЕНТ OS CORE */}
+
+
+
+
+
+
+
Ядро Системы (OS)
+
Интеллектуальная операционная система, объединяющая все устройства в единый организм.
+
+
+
+ {/* 3. BIOMETRICS */}
+
+
+
+
+
+ Биометрия
+ Бесключевой доступ и персональные сценарии на основе отпечатка пальца.
+
+
+
+
+
+
+ {/* FOOTER CTA */}
+
+
+ Готовы к будущему?
+
+ СВЯЗАТЬСЯ С НАМИ
+
+
+
+
+ );
+};
+
+export default Home;
\ No newline at end of file
diff --git a/src/pages/Kabinet.jsx b/src/pages/Kabinet.jsx
new file mode 100644
index 0000000..66fe4fd
--- /dev/null
+++ b/src/pages/Kabinet.jsx
@@ -0,0 +1,240 @@
+import React, { useState, useEffect } from 'react';
+import axios from 'axios';
+import { motion } from 'framer-motion';
+import {
+ User, Calendar, Shield, Key, Lock,
+ Package, Clock, CheckCircle, Truck, XCircle, Send, ExternalLink
+} from 'lucide-react';
+import UserAvatar from '../components/UserAvatar';
+
+const API_URL = 'https://diplomnexus.aptcloud.ru';
+
+const Kabinet = ({ user }) => {
+ const [orders, setOrders] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [passForm, setPassForm] = useState({ oldPassword: '', newPassword: '' });
+ const [passMsg, setPassMsg] = useState('');
+ const [passError, setPassError] = useState('');
+ const [telegramInfo, setTelegramInfo] = useState(null);
+
+ useEffect(() => {
+ if (user) {
+ fetchUserOrders();
+ }
+ }, [user]);
+
+ const fetchUserOrders = async () => {
+ try {
+ const token = localStorage.getItem('token');
+ // Запрашиваем /orders. После исправления index.js придут только заказы этого юзера
+ const res = await axios.get(`${API_URL}/orders`, {
+ headers: { Authorization: `Bearer ${token}` }
+ });
+ const userOrders = res.data;
+ setOrders(userOrders);
+
+ // Ищем привязанный TG (проверяем, есть ли хоть в одном заказе telegram_chat_id)
+ const linkedOrder = userOrders.find(o => o.telegram_chat_id);
+ if (linkedOrder) {
+ setTelegramInfo({
+ id: linkedOrder.telegram_chat_id,
+ username: linkedOrder.telegram_username || user.name,
+ connected: true
+ });
+ }
+ } catch (e) {
+ console.error("Ошибка загрузки заказов:", e);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleChangePassword = async (e) => {
+ e.preventDefault();
+ setPassMsg(''); setPassError('');
+ if (passForm.newPassword.length < 6) return setPassError('Мин. 6 символов');
+ try {
+ const token = localStorage.getItem('token');
+ await axios.put(`${API_URL}/users/password`, passForm, { headers: { Authorization: `Bearer ${token}` } });
+ setPassMsg('Пароль обновлен!'); setPassForm({ oldPassword: '', newPassword: '' });
+ } catch (e) { setPassError(e.response?.data?.error || 'Ошибка смены пароля'); }
+ };
+
+ const handleCancelOrder = async (orderId) => {
+ if (!window.confirm('Вы уверены, что хотите отменить заказ?')) return;
+ try {
+ await axios.post(`${API_URL}/api/internal/orders/${orderId}/cancel-by-user`);
+ // Обновляем список локально, чтобы не дергать сервер лишний раз (или можно вызвать fetchUserOrders)
+ setOrders(orders.map(o => o.id === orderId ? { ...o, status: 'cancelled' } : o));
+ alert('Заказ отменен.');
+ } catch (e) { alert('Не удалось отменить.'); }
+ };
+
+ const getStatusBadge = (status) => {
+ switch (status) {
+ case 'placed': return РАЗМЕЩЕН ;
+ case 'processing': return В РАБОТЕ ;
+ case 'shipped': return В ПУТИ ;
+ case 'completed': return ВЫПОЛНЕН ;
+ case 'cancelled': return ОТМЕНЕН ;
+ default: return {status} ;
+ }
+ };
+
+ if (!user) return Загрузка профиля...
;
+
+ return (
+
+
+ Личный Кабинет
+
+
+
+
+ {/* ЛЕВАЯ КОЛОНКА: ПРОФИЛЬ */}
+
+
+ {/* КАРТОЧКА ПОЛЬЗОВАТЕЛЯ */}
+
+
+
+
+
+
+
{user.name}
+
{user.email}
+
+
+
+
+
+
+
Дата регистрации
+
{user.created_at ? new Date(user.created_at).toLocaleDateString() : '-'}
+
+
+
+
+
+
+ {/* TELEGRAM */}
+
+ TELEGRAM
+ {telegramInfo ? (
+
+
+
+ {telegramInfo.username[0].toUpperCase()}
+
+
+
+
✅ Аккаунт привязан
+
+ ) : (
+
+
Аккаунт не привязан
+
Совершите покупку через бота, чтобы привязать уведомления.
+
+ )}
+
+
+ {/* СМЕНА ПАРОЛЯ */}
+
+ ПАРОЛЬ
+
+
+
+ setPassForm({...passForm, oldPassword: e.target.value})} required/>
+
+
+
+ setPassForm({...passForm, newPassword: e.target.value})} required/>
+
+ {passMsg && {passMsg}
}
+ {passError && {passError}
}
+ ОБНОВИТЬ
+
+
+
+
+ {/* ПРАВАЯ КОЛОНКА: ЗАКАЗЫ */}
+
+
+
МОИ ЗАКАЗЫ
+
+ {loading ? (
+
Загрузка списка заказов...
+ ) : orders.length === 0 ? (
+
+
+
История заказов пуста
+
+ ) : (
+
+ {orders.map((o) => (
+
+
+ {/* ХЕДЕР ЗАКАЗА */}
+
+
+
+ #{o.order_number || o.id}
+
+
+
{getStatusBadge(o.status)}
+
{new Date(o.created_at).toLocaleString()}
+
+
+
+ {(o.status === 'placed' || o.status === 'processing') && (
+
handleCancelOrder(o.id)} className="flex items-center gap-1 text-[10px] text-red-400 bg-red-500/10 px-3 py-1.5 rounded-lg border border-red-500/20 hover:bg-red-500 hover:text-white transition-colors">
+ ОТМЕНИТЬ ЗАКАЗ
+
+ )}
+
+
+ {/* ТЕЛО ЗАКАЗА */}
+
+
+
Содержание
+
{o.content}
+
+ {o.delivery_address && (
+
+
+
{o.delivery_address}
+
+ )}
+
+
+
+
+ Сумма
+ {o.total} ₽
+
+
+ {o.payment_status === 'paid' ? 'ОПЛАЧЕНО' : 'НЕ ОПЛАЧЕНО'}
+
+
+
+
+ ))}
+
+ )}
+
+
+
+
+ );
+};
+export default Kabinet;
\ No newline at end of file
diff --git a/src/pages/Payment.jsx b/src/pages/Payment.jsx
new file mode 100644
index 0000000..c5d2643
--- /dev/null
+++ b/src/pages/Payment.jsx
@@ -0,0 +1,293 @@
+import { useState, useEffect, useRef } from 'react';
+import { useNavigate } from 'react-router-dom';
+import axios from 'axios';
+import { CreditCard, MapPin, Calendar, User, Phone, Navigation, Loader2 } from 'lucide-react';
+import { motion } from 'framer-motion';
+import Iphone from '../components/Iphone';
+
+const Payment = () => {
+ const [cart, setCart] = useState([]);
+ const [loading, setLoading] = useState(false);
+
+ // Состояние формы
+ const [form, setForm] = useState({
+ fio: '',
+ phone: '',
+ address: '',
+ date: '',
+ coords: '52.5400, 103.8800'
+ });
+
+ const mapInstance = useRef(null);
+ const navigate = useNavigate();
+
+ // 1. ЗАГРУЗКА КОРЗИНЫ
+ useEffect(() => {
+ const savedCart = JSON.parse(localStorage.getItem('tempCart') || '[]');
+ setCart(savedCart);
+ if(savedCart.length === 0) navigate('/dashboard');
+ }, []);
+
+ const total = cart.reduce((acc, item) => acc + item.price * item.qty, 0);
+
+ // 2. ИНИЦИАЛИЗАЦИЯ КАРТЫ
+ useEffect(() => {
+ if (!window.ymaps) {
+ const script = document.createElement('script');
+ script.src = "https://api-maps.yandex.ru/2.1/?apikey=794e6377-6202-426f-8706-930263f350df&lang=ru_RU";
+ script.async = true;
+ document.body.appendChild(script);
+ script.onload = initMap;
+ } else {
+ initMap();
+ }
+
+ return () => {
+ if (mapInstance.current) {
+ mapInstance.current = null;
+ }
+ };
+ }, []);
+
+ const initMap = () => {
+ window.ymaps.ready(() => {
+ if (mapInstance.current) return;
+
+ const map = new window.ymaps.Map("yandex-map", {
+ center: [52.5400, 103.8800], // Ангарск
+ zoom: 13,
+ controls: ['zoomControl']
+ });
+
+ mapInstance.current = map;
+
+ const myPlacemark = new window.ymaps.Placemark([52.5400, 103.8800], {
+ hintContent: 'Место установки'
+ }, {
+ preset: 'islands#redDotIcon'
+ });
+
+ map.geoObjects.add(myPlacemark);
+
+ map.events.add('click', function (e) {
+ const coords = e.get('coords');
+ myPlacemark.geometry.setCoordinates(coords);
+
+ const lat = coords[0].toFixed(4);
+ const lon = coords[1].toFixed(4);
+
+ const streets = ['ул. Карла Маркса', 'ул. Ленина', '12-й микрорайон', '85-й квартал', 'ул. Космонавтов', 'ул. Чайковского', 'ул. Горького', 'мкр. Китова'];
+ const randomStreet = streets[Math.floor(Math.random() * streets.length)];
+ const randomHouse = Math.floor(Math.random() * 80) + 1;
+ const detectedAddress = `г. Ангарск, ${randomStreet}, д. ${randomHouse}`;
+
+ setForm(prev => ({
+ ...prev,
+ coords: `${lat}, ${lon}`,
+ address: detectedAddress
+ }));
+ });
+ });
+ };
+
+ // --- ЛОГИКА ОПЛАТЫ ---
+ const handlePay = async (e) => {
+ e.preventDefault();
+ setLoading(true);
+
+ try {
+ const token = localStorage.getItem('token');
+
+ // 1. СОЗДАЕМ ЗАКАЗ В БАЗЕ
+ const res = await axios.post('https://diplomnexus.aptcloud.ru/orders', {
+ cart: cart,
+ delivery: form
+ }, {
+ headers: { Authorization: `Bearer ${token}` }
+ });
+
+ const orderId = res.data.orderId;
+
+ if (!orderId) {
+ alert('Ошибка сервера: не вернулся номер заказа.');
+ setLoading(false);
+ return;
+ }
+
+ // 2. ЧИСТИМ КОРЗИНУ
+ localStorage.removeItem('cart');
+ localStorage.removeItem('tempCart');
+
+ // 3. ЖЕСТКИЙ РЕДИРЕКТ В ТЕЛЕГРАМ
+ // Окно браузера сразу перейдет в ТГ
+ window.location.href = `https://t.me/oplata_umniydombot?start=${orderId}`;
+
+ } catch (e) {
+ console.error(e);
+ setLoading(false);
+ alert('Ошибка при создании заказа.');
+ }
+ };
+
+ return (
+
+
+
+ {/* ЛЕВАЯ КОЛОНКА */}
+
+
+
+
+
+
+ Управляйте установкой через приложение Nexus Home после оплаты
+
+
+
+
+
+
+
+
+
+
Настройка
+
Данные для выезда инженера
+
+
+
+
+
+
+
ФИО Клиента
+
+
+ setForm({...form, fio: e.target.value})}
+ />
+
+
+
+
+
+
+
Адрес (Кликните на карту)
+
+
+ setForm({...form, address: e.target.value})}
+ />
+
+
+
+
+
Дата монтажа
+
+
+ setForm({...form, date: e.target.value})}
+ />
+
+
+
+
+
+ Геолокация объекта
+
+ {form.coords}
+
+
+
+
+
+
+
+
+
+ {/* ПРАВАЯ КОЛОНКА: ЧЕК */}
+
+
+
+
+
+ СВОДКА
+
+
+
+ {cart.map(item => (
+
+
+
+
+
+
+ {item.name}
+ x{item.qty}
+
+
+
{item.price * item.qty}₽
+
+ ))}
+
+
+
+
+ Оборудование:
+ {total}₽
+
+
+ Монтаж:
+ БЕСПЛАТНО
+
+
+ ИТОГО:
+ {total}₽
+
+
+
+
+ {loading ? <> ПЕРЕХОД...> : 'ОПЛАТИТЬ ЧЕРЕЗ TELEGRAM'}
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default Payment;
\ No newline at end of file
diff --git a/src/pages/tables/CamerasTable.jsx b/src/pages/tables/CamerasTable.jsx
new file mode 100644
index 0000000..bcfc170
--- /dev/null
+++ b/src/pages/tables/CamerasTable.jsx
@@ -0,0 +1,261 @@
+import React, { useState, useEffect } from 'react';
+import axios from 'axios';
+import { Plus, X, Eye, ChevronLeft, ChevronRight, AlertCircle, Video } from 'lucide-react';
+import ActionMenu from '../../components/ActionMenu';
+
+const API_URL = 'https://diplomnexus.aptcloud.ru';
+
+const CamerasTable = ({ user }) => {
+ const [cameras, setCameras] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [isModalOpen, setIsModalOpen] = useState(false);
+ const [viewData, setViewData] = useState(null);
+ const [formData, setFormData] = useState({ id: null, name: '', price: '', category: 'cameras', description: '', image: null });
+
+ // ПАГИНАЦИЯ
+ const [currentPage, setCurrentPage] = useState(1);
+ const itemsPerPage = 10;
+
+ // ОПРЕДЕЛЕНИЕ ПРАВ
+ const isAdmin = user?.role === 'admin' || user?.email === 'admin@mail.ru';
+
+ useEffect(() => {
+ fetchCameras();
+ }, []);
+
+ const fetchCameras = async () => {
+ setLoading(true);
+ try {
+ // Запрос именно к таблице cameras
+ const res = await axios.get(`${API_URL}/api/cameras`);
+ setCameras(res.data);
+ } catch(e) {
+ console.error("Ошибка при загрузке камер:", e);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleDelete = async (id) => {
+ if (!isAdmin) return;
+ if(window.confirm('Удалить камеру из системы мониторинга?')) {
+ try {
+ await axios.delete(`${API_URL}/admin/cameras/${id}`, {
+ headers: { Authorization: `Bearer ${localStorage.getItem('token')}` }
+ });
+ fetchCameras();
+ } catch(e) { alert("Ошибка удаления"); }
+ }
+ };
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ if (!isAdmin) return;
+
+ const data = new FormData();
+ Object.keys(formData).forEach(k => {
+ if (formData[k] !== null) data.append(k, formData[k]);
+ });
+
+ const cfg = { headers: { Authorization: `Bearer ${localStorage.getItem('token')}` } };
+ try {
+ if(formData.id) {
+ await axios.put(`${API_URL}/admin/cameras/${formData.id}`, formData, cfg);
+ } else {
+ await axios.post(`${API_URL}/admin/cameras`, data, cfg);
+ }
+ setIsModalOpen(false);
+ fetchCameras();
+ } catch(e) { alert("Ошибка сохранения"); }
+ };
+
+ const getImageUrl = (img) => {
+ if (!img) return null;
+ if (img.startsWith('http')) return img;
+ return `${API_URL}/${img}`;
+ };
+
+ const indexOfLastItem = currentPage * itemsPerPage;
+ const indexOfFirstItem = indexOfLastItem - itemsPerPage;
+ const currentItems = cameras.slice(indexOfFirstItem, indexOfLastItem);
+ const totalPages = Math.ceil(cameras.length / itemsPerPage);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ ВИДЕОНАБЛЮДЕНИЕ
+
+
Системы безопасности и онлайн-мониторинга ({cameras.length} ед.)
+
+
+ {isAdmin && (
+
{ setFormData({ id: null, name: '', price: '', category: 'cameras', description: '', image: null }); setIsModalOpen(true); }}
+ className="btn-neon px-4 py-2 text-xs font-bold flex gap-2"
+ >
+ ДОБАВИТЬ КАМЕРУ
+
+ )}
+
+
+
+ {loading ? (
+
SCANNING DATABASE...
+ ) : (
+ <>
+
+
+
+ VIEW
+ Модель
+ SKU / ID
+ Тариф / Цена
+ Управление
+
+
+
+ {currentItems.map((p) => (
+
+
+ {p.image ? (
+
+ ) : (
+ NO_SIGNAL
+ )}
+
+ {p.name}
+ {p.sku || `CAM-${p.id}`}
+ {p.price} ₽
+
+ setViewData(p)} className="p-2 text-gray-400 hover:text-[var(--accent-color)] transition-colors">
+
+
+ {isAdmin && (
+ { setFormData({...p, category: 'cameras'}); setIsModalOpen(true); }}
+ onDelete={() => handleDelete(p.id)}
+ />
+ )}
+
+
+ ))}
+
+
+
+ {totalPages > 1 && (
+
+ setCurrentPage(p => Math.max(1, p - 1))} disabled={currentPage === 1} className="p-2 disabled:opacity-30 hover:text-[var(--accent-color)]">
+ PAGE {currentPage} / {totalPages}
+ setCurrentPage(p => Math.min(totalPages, p + 1))} disabled={currentPage === totalPages} className="p-2 disabled:opacity-30 hover:text-[var(--accent-color)]">
+
+ )}
+ >
+ )}
+
+
+
+ {/* MODAL EDIT/CREATE */}
+ {isModalOpen && isAdmin && (
+
+ )}
+
+ {/* VIEW MODAL */}
+ {viewData && (
+
+
+
setViewData(null)} className="absolute top-2 right-2 z-10 bg-black/50 p-1 rounded-full text-white hover:bg-[var(--accent-color)] hover:text-black transition-colors">
+
+
+ {viewData.image ? (
+
+ ) : (
+
+ )}
+
+
+
+ {viewData.price} ₽
+
+
+
+
+
+
+
{viewData.name}
+
ID_SOURCE: {viewData.id} / {viewData.sku || 'NEXUS_VISION'}
+
+
+
+
+ Объектив
+ 4K Ultra HD
+
+
+ Запись
+ Облако / SD
+
+
+
+
+
Техническая сводка:
+
+ {viewData.description || 'Данные о характеристиках видеосенсора не заполнены.'}
+
+
+
+
+
+ )}
+
+ );
+};
+
+export default CamerasTable;
\ No newline at end of file
diff --git a/src/pages/tables/HubsTable.jsx b/src/pages/tables/HubsTable.jsx
new file mode 100644
index 0000000..6ccda96
--- /dev/null
+++ b/src/pages/tables/HubsTable.jsx
@@ -0,0 +1,252 @@
+import React, { useState, useEffect } from 'react';
+import axios from 'axios';
+import { Plus, X, Eye, ChevronLeft, ChevronRight, AlertCircle } from 'lucide-react';
+import ActionMenu from '../../components/ActionMenu';
+
+const API_URL = 'https://diplomnexus.aptcloud.ru';
+
+const HubsTable = ({ user }) => {
+ const [hubs, setHubs] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [isModalOpen, setIsModalOpen] = useState(false);
+ const [viewData, setViewData] = useState(null);
+ const [formData, setFormData] = useState({ id: null, name: '', price: '', category: 'hubs', description: '', image: null });
+
+ // ПАГИНАЦИЯ
+ const [currentPage, setCurrentPage] = useState(1);
+ const itemsPerPage = 10;
+
+ // ОПРЕДЕЛЕНИЕ ПРАВ
+ const isAdmin = user?.role === 'admin' || user?.email === 'admin@mail.ru';
+
+ useEffect(() => {
+ fetchHubs();
+ }, []);
+
+ const fetchHubs = async () => {
+ setLoading(true);
+ try {
+ // Запрос именно к таблице hubs
+ const res = await axios.get(`${API_URL}/api/hubs`);
+ setHubs(res.data);
+ } catch(e) {
+ console.error("Ошибка при загрузке хабов:", e);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleDelete = async (id) => {
+ if (!isAdmin) return;
+ if(window.confirm('Удалить этот хаб из базы?')) {
+ try {
+ await axios.delete(`${API_URL}/admin/hubs/${id}`, {
+ headers: { Authorization: `Bearer ${localStorage.getItem('token')}` }
+ });
+ fetchHubs();
+ } catch(e) { alert("Ошибка удаления"); }
+ }
+ };
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ if (!isAdmin) return;
+
+ const data = new FormData();
+ // Добавляем все поля в FormData
+ Object.keys(formData).forEach(k => {
+ if (formData[k] !== null) data.append(k, formData[k]);
+ });
+
+ const cfg = { headers: { Authorization: `Bearer ${localStorage.getItem('token')}` } };
+ try {
+ if(formData.id) {
+ // Обновление
+ await axios.put(`${API_URL}/admin/hubs/${formData.id}`, formData, cfg);
+ } else {
+ // Создание нового
+ await axios.post(`${API_URL}/admin/hubs`, data, cfg);
+ }
+ setIsModalOpen(false);
+ fetchHubs();
+ } catch(e) { alert("Ошибка сохранения"); }
+ };
+
+ const getImageUrl = (img) => {
+ if (!img) return null;
+ if (img.startsWith('http')) return img;
+ return `${API_URL}/${img}`;
+ };
+
+ const indexOfLastItem = currentPage * itemsPerPage;
+ const indexOfFirstItem = indexOfLastItem - itemsPerPage;
+ const currentItems = hubs.slice(indexOfFirstItem, indexOfLastItem);
+ const totalPages = Math.ceil(hubs.length / itemsPerPage);
+
+ return (
+
+
+
+
+
+ УМНЫЕ ХАБЫ ({hubs.length})
+
+ {isAdmin && (
+
{ setFormData({ id: null, name: '', price: '', category: 'hubs', description: '', image: null }); setIsModalOpen(true); }}
+ className="btn-neon px-4 py-2 text-xs font-bold flex gap-2"
+ >
+ ДОБАВИТЬ ХАБ
+
+ )}
+
+
+
+ {loading ? (
+
Загрузка данных из таблицы Hubs...
+ ) : (
+ <>
+
+
+
+ IMG
+ Название устройства
+ Артикул (SKU)
+ Цена
+ Действия
+
+
+
+ {currentItems.map((p) => (
+
+
+ {p.image ? (
+
+ ) : (
+ НЕТ
+ )}
+
+ {p.name}
+ {p.sku || 'N/A'}
+ {p.price} ₽
+
+ setViewData(p)} className="p-2 text-gray-400 hover:text-[var(--accent-color)] transition-colors">
+
+
+ {isAdmin && (
+ { setFormData({...p, category: 'hubs'}); setIsModalOpen(true); }}
+ onDelete={() => handleDelete(p.id)}
+ />
+ )}
+
+
+ ))}
+
+
+
+ {totalPages > 1 && (
+
+ setCurrentPage(p => Math.max(1, p - 1))} disabled={currentPage === 1} className="p-2 disabled:opacity-30 hover:text-[var(--accent-color)]">
+ СТР. {currentPage} ИЗ {totalPages}
+ setCurrentPage(p => Math.min(totalPages, p + 1))} disabled={currentPage === totalPages} className="p-2 disabled:opacity-30 hover:text-[var(--accent-color)]">
+
+ )}
+ >
+ )}
+
+
+
+ {/* MODAL EDIT/CREATE */}
+ {isModalOpen && isAdmin && (
+
+ )}
+
+ {/* VIEW MODAL */}
+ {viewData && (
+
+
+
setViewData(null)} className="absolute top-2 right-2 z-10 bg-black/50 p-1 rounded-full text-white hover:bg-[var(--accent-color)] hover:text-black transition-colors">
+
+
+ {viewData.image ? (
+
+ ) : (
+
+ )}
+
+
+ {viewData.price} ₽
+
+
+
+
+
+
+
{viewData.name}
+
HUB_NODE_ID: {viewData.id} / {viewData.sku || 'NO_SKU'}
+
+
+
+
+ Тип системы
+ Центральный Хаб
+
+
+ Склад
+ Доступно
+
+
+
+
+
Спецификация:
+
+ {viewData.description || 'Технические характеристики не указаны.'}
+
+
+
+
+
+ )}
+
+ );
+};
+
+export default HubsTable;
\ No newline at end of file
diff --git a/src/pages/tables/LightingTable.jsx b/src/pages/tables/LightingTable.jsx
new file mode 100644
index 0000000..d8c9b22
--- /dev/null
+++ b/src/pages/tables/LightingTable.jsx
@@ -0,0 +1,257 @@
+import React, { useState, useEffect } from 'react';
+import axios from 'axios';
+import { Plus, X, Eye, ChevronLeft, ChevronRight, AlertCircle, Lightbulb } from 'lucide-react';
+import ActionMenu from '../../components/ActionMenu';
+
+const API_URL = 'https://diplomnexus.aptcloud.ru';
+
+const LightingTable = ({ user }) => {
+ const [lighting, setLighting] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [isModalOpen, setIsModalOpen] = useState(false);
+ const [viewData, setViewData] = useState(null);
+ const [formData, setFormData] = useState({ id: null, name: '', price: '', category: 'lighting', description: '', image: null });
+
+ // ПАГИНАЦИЯ
+ const [currentPage, setCurrentPage] = useState(1);
+ const itemsPerPage = 10;
+
+ // ОПРЕДЕЛЕНИЕ ПРАВ
+ const isAdmin = user?.role === 'admin' || user?.email === 'admin@mail.ru';
+
+ useEffect(() => {
+ fetchLighting();
+ }, []);
+
+ const fetchLighting = async () => {
+ setLoading(true);
+ try {
+ // Запрос именно к таблице lighting
+ const res = await axios.get(`${API_URL}/api/lighting`);
+ setLighting(res.data);
+ } catch(e) {
+ console.error("Ошибка при загрузке освещения:", e);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleDelete = async (id) => {
+ if (!isAdmin) return;
+ if(window.confirm('Удалить осветительный прибор из базы?')) {
+ try {
+ await axios.delete(`${API_URL}/admin/lighting/${id}`, {
+ headers: { Authorization: `Bearer ${localStorage.getItem('token')}` }
+ });
+ fetchLighting();
+ } catch(e) { alert("Ошибка удаления"); }
+ }
+ };
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ if (!isAdmin) return;
+
+ const data = new FormData();
+ Object.keys(formData).forEach(k => {
+ if (formData[k] !== null) data.append(k, formData[k]);
+ });
+
+ const cfg = { headers: { Authorization: `Bearer ${localStorage.getItem('token')}` } };
+ try {
+ if(formData.id) {
+ await axios.put(`${API_URL}/admin/lighting/${formData.id}`, formData, cfg);
+ } else {
+ await axios.post(`${API_URL}/admin/lighting`, data, cfg);
+ }
+ setIsModalOpen(false);
+ fetchLighting();
+ } catch(e) { alert("Ошибка сохранения"); }
+ };
+
+ const getImageUrl = (img) => {
+ if (!img) return null;
+ if (img.startsWith('http')) return img;
+ return `${API_URL}/${img}`;
+ };
+
+ const indexOfLastItem = currentPage * itemsPerPage;
+ const indexOfFirstItem = indexOfLastItem - itemsPerPage;
+ const currentItems = lighting.slice(indexOfFirstItem, indexOfLastItem);
+ const totalPages = Math.ceil(lighting.length / itemsPerPage);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ УМНОЕ ОСВЕЩЕНИЕ
+
+
Управление светом и атмосферой ({lighting.length} поз.)
+
+
+ {isAdmin && (
+
{ setFormData({ id: null, name: '', price: '', category: 'lighting', description: '', image: null }); setIsModalOpen(true); }}
+ className="btn-neon px-4 py-2 text-xs font-bold flex gap-2"
+ >
+ ДОБАВИТЬ ЛАМПУ
+
+ )}
+
+
+
+ {loading ? (
+
LIGHT_INIT_DATABASE...
+ ) : (
+ <>
+
+
+
+ ФОТО
+ Наименование
+ SKU / Арт.
+ Стоимость
+ Инфо
+
+
+
+ {currentItems.map((p) => (
+
+
+ {p.image ? (
+
+ ) : (
+ DARK
+ )}
+
+ {p.name}
+ {p.sku || `LUM-${p.id}`}
+ {p.price} ₽
+
+ setViewData(p)} className="p-2 text-gray-400 hover:text-[var(--accent-color)] transition-colors">
+
+
+ {isAdmin && (
+ { setFormData({...p, category: 'lighting'}); setIsModalOpen(true); }}
+ onDelete={() => handleDelete(p.id)}
+ />
+ )}
+
+
+ ))}
+
+
+
+ {totalPages > 1 && (
+
+ setCurrentPage(p => Math.max(1, p - 1))} disabled={currentPage === 1} className="p-2 disabled:opacity-30 hover:text-[var(--accent-color)]">
+ STATION {currentPage} / {totalPages}
+ setCurrentPage(p => Math.min(totalPages, p + 1))} disabled={currentPage === totalPages} className="p-2 disabled:opacity-30 hover:text-[var(--accent-color)]">
+
+ )}
+ >
+ )}
+
+
+
+ {/* MODAL EDIT/CREATE */}
+ {isModalOpen && isAdmin && (
+
+ )}
+
+ {/* VIEW MODAL */}
+ {viewData && (
+
+
+
setViewData(null)} className="absolute top-2 right-2 z-10 bg-black/50 p-1 rounded-full text-white hover:bg-[var(--accent-color)] hover:text-black transition-colors">
+
+
+ {viewData.image ? (
+
+ ) : (
+
+ )}
+
+
+ {viewData.price} ₽
+
+
+
+
+
+
+
{viewData.name}
+
ID_SOURCE: {viewData.id} / {viewData.sku || 'NEXUS_LIGHT'}
+
+
+
+
+ Спектр
+ RGB + White
+
+
+ Ресурс
+ 50,000 Часов
+
+
+
+
+
Световая схема и описание:
+
+ {viewData.description || 'Сведения об интенсивности и цветопередаче отсутствуют.'}
+
+
+
+
+
+ )}
+
+ );
+};
+
+export default LightingTable;
\ No newline at end of file
diff --git a/src/pages/tables/LogsTable.jsx b/src/pages/tables/LogsTable.jsx
new file mode 100644
index 0000000..4ab3175
--- /dev/null
+++ b/src/pages/tables/LogsTable.jsx
@@ -0,0 +1,59 @@
+import React, { useState, useEffect } from 'react';
+import axios from 'axios';
+import { Eye, X, Terminal, AlertTriangle, ChevronLeft, ChevronRight } from 'lucide-react';
+
+const LogsTable = ({ user }) => {
+ const [logs, setLogs] = useState([]);
+ const [viewData, setViewData] = useState(null);
+
+ // ПАГИНАЦИЯ
+ const [currentPage, setCurrentPage] = useState(1);
+ const itemsPerPage = 10;
+
+ const isAdmin = user?.role === 'admin';
+
+ useEffect(() => {
+ if (isAdmin) axios.get('https://diplomnexus.aptcloud.ru/logs', { headers: { Authorization: `Bearer ${localStorage.getItem('token')}` } }).then(res => setLogs(res.data));
+ }, [isAdmin]);
+
+ const indexOfLastItem = currentPage * itemsPerPage;
+ const indexOfFirstItem = indexOfLastItem - itemsPerPage;
+ const currentItems = logs.slice(indexOfFirstItem, indexOfLastItem);
+ const totalPages = Math.ceil(logs.length / itemsPerPage);
+
+ if (!isAdmin) return ;
+
+ return (
+
+
+
SYSTEM LOGS ({logs.length})
+
+
+
+ Time User Method Route Status View
+
+
+ {currentItems.map((l) => (
+
+ {new Date(l.timestamp).toLocaleTimeString()}
+ {l.username}
+ {l.method}
+ {l.url}
+ =400?'text-red-500':'text-green-500'}`}>{l.status_code}
+ setViewData(l)}>
+
+ ))}
+
+
+
+ setCurrentPage(p => Math.max(1, p - 1))} disabled={currentPage === 1} className="p-2 disabled:opacity-30 hover:text-red-500">
+ PAGE {currentPage} OF {totalPages}
+ setCurrentPage(p => Math.min(totalPages, p + 1))} disabled={currentPage === totalPages} className="p-2 disabled:opacity-30 hover:text-red-500">
+
+
+
+ {viewData && (
setViewData(null)} className="absolute top-4 right-4 text-red-500"> LOG DETAIL #{viewData.id} {JSON.stringify(viewData, null, 2)} )}
+
+ );
+};
+export default LogsTable;
\ No newline at end of file
diff --git a/src/pages/tables/MessagesTable.jsx b/src/pages/tables/MessagesTable.jsx
new file mode 100644
index 0000000..71022a0
--- /dev/null
+++ b/src/pages/tables/MessagesTable.jsx
@@ -0,0 +1,204 @@
+import React, { useState, useEffect } from 'react';
+import axios from 'axios';
+import { Eye, Mail, User, Shield, RefreshCw, AlertCircle, X, ChevronLeft, ChevronRight } from 'lucide-react';
+import ActionMenu from '../../components/ActionMenu';
+const API_URL = 'https://diplomnexus.aptcloud.ru';
+const MessagesTable = ({ user }) => {
+ const [msgs, setMsgs] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+ const [viewData, setViewData] = useState(null);
+ const [currentPage, setCurrentPage] = useState(1);
+ const itemsPerPage = 10;
+ const isAdmin =
+ user?.role === 'admin' ||
+ user?.email === 'admin@mail.ru' ||
+ user?.name === 'seth1nk';
+ useEffect(() => {
+ fetchMessages();
+ const interval = setInterval(fetchMessages, 10000);
+ return () => clearInterval(interval);
+ }, []);
+ const fetchMessages = async () => {
+ const token = localStorage.getItem('token');
+ if (!token) return;
+ try {
+ setLoading(true);
+ const res = await axios.get(`${API_URL}/admin/messages`, {
+ headers: { Authorization: `Bearer ${token}` }
+ });
+ const sorted = res.data.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
+ setMsgs(sorted);
+ setError(null);
+ } catch (e) {
+ console.error("Fetch error:", e);
+ if (e.response && e.response.status === 403) {
+ setError("У вас нет прав администратора для просмотра всех сообщений.");
+ } else if (e.response && e.response.status === 404) {
+ setError("API '/admin/messages' не найден. Проверьте index.js.");
+ } else {
+ setError("Не удалось загрузить сообщения. Проверьте сервер.");
+ }
+ } finally {
+ setLoading(false);
+ }
+ };
+ const handleDelete = async (id) => {
+ if (!isAdmin) return alert("Доступ запрещен");
+ if (window.confirm("Удалить сообщение?")) {
+
+ alert("Функция удаления пока не настроена на сервере.");
+ }
+ };
+ const indexOfLastItem = currentPage * itemsPerPage;
+ const indexOfFirstItem = indexOfLastItem - itemsPerPage;
+ const currentItems = msgs.slice(indexOfFirstItem, indexOfLastItem);
+ const totalPages = Math.ceil(msgs.length / itemsPerPage);
+ const renderStatus = (m) => {
+ if (m.is_admin) return ОТВЕТ ;
+ if (m.is_read) return ПРОЧИТАНО ;
+ return НОВОЕ ;
+ };
+
+ return (
+
+
+
+ {/* ЗАГОЛОВОК */}
+
+
+
+ ПОДДЕРЖКА / ЧАТ ({msgs.length})
+
+
+
+
+
+
+
+
+ {/* ТАБЛИЦА */}
+
+
+ {error ? (
+
+ ) : (
+ <>
+
+
+
+ #
+ Дата
+ Отправитель
+ Тема / Текст
+ Статус
+ Инфо
+
+
+
+ {msgs.length === 0 ? (
+
+
+ {loading ? "Загрузка..." : "Сообщений пока нет"}
+
+
+ ) : currentItems.map((m) => (
+
+
+
+ {new Date(m.created_at).toLocaleString()}
+
+
+
+
+
+ {m.is_admin ? : }
+
+
+
+ {m.user_name || 'Аноним'}
+
+
{m.email}
+
+
+
+
+
+
+
{m.subject}
+
{m.text}
+
+
+
+ {renderStatus(m)}
+
+
+ setViewData(m)} className="p-2 hover:text-[var(--accent-color)] text-gray-400" title="Просмотреть">
+
+
+ {/* Меню действий только для админа */}
+ {isAdmin && (
+ {}}
+ onDelete={() => handleDelete(m.id)}
+ />
+ )}
+
+
+ ))}
+
+
+
+ {/* ПАГИНАЦИЯ */}
+ {totalPages > 1 && (
+
+ setCurrentPage(p => Math.max(1, p - 1))} disabled={currentPage === 1} className="p-2 hover:text-[var(--accent-color)] disabled:opacity-30">
+ СТР. {currentPage} / {totalPages}
+ setCurrentPage(p => Math.min(totalPages, p + 1))} disabled={currentPage === totalPages} className="p-2 hover:text-[var(--accent-color)] disabled:opacity-30">
+
+ )}
+ >
+ )}
+
+
+
+ {/* МОДАЛКА ПРОСМОТРА */}
+ {viewData && (
+
+
+
setViewData(null)} className="absolute top-4 right-4 hover:text-red-500 transition-colors">
+
+
+
+
+
+ {viewData.user_name ? viewData.user_name[0].toUpperCase() : '?'}
+
+
+
{viewData.subject}
+
{viewData.email} • {new Date(viewData.created_at).toLocaleString()}
+
+
+
+
+ {viewData.text}
+
+
+
+ IP: {viewData.ip || 'Скрыт'}
+ ID: {viewData.id}
+
+
+
+ )}
+
+ );
+};
+
+export default MessagesTable;
\ No newline at end of file
diff --git a/src/pages/tables/OrdersTable.jsx b/src/pages/tables/OrdersTable.jsx
new file mode 100644
index 0000000..d0b9320
--- /dev/null
+++ b/src/pages/tables/OrdersTable.jsx
@@ -0,0 +1,351 @@
+import React, { useState, useEffect } from 'react';
+import axios from 'axios';
+import {
+ Eye, X, ChevronLeft, ChevronRight,
+ Package, Truck, CheckCircle, AlertTriangle, ArrowRight, XCircle,
+ Clock, RefreshCw, AlertCircle
+} from 'lucide-react';
+
+const API_URL = 'https://diplomnexus.aptcloud.ru';
+
+const OrdersTable = ({ user }) => {
+ const [orders, setOrders] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+ const [viewData, setViewData] = useState(null);
+
+ // ПАГИНАЦИЯ
+ const [currentPage, setCurrentPage] = useState(1);
+ const itemsPerPage = 10;
+
+ // Проверка прав (Админ или Спец. юзеры)
+ const isAdmin =
+ user?.role === 'admin' ||
+ user?.email === 'admin@mail.ru' ||
+ user?.name === 'seth1nk';
+
+ useEffect(() => {
+ fetchOrders();
+ const interval = setInterval(fetchOrders, 15000); // Автообновление
+ return () => clearInterval(interval);
+ }, [user]);
+
+ const fetchOrders = async () => {
+ const token = localStorage.getItem('token');
+ if (!token) return;
+
+ try {
+ setLoading(true);
+
+ // --- ИЗМЕНЕНИЕ: ТЕПЕРЬ ВСЕ ИДУТ НА ОДИН РОУТ ---
+ const endpoint = '/admin/orders';
+ // -----------------------------------------------
+
+ const res = await axios.get(`${API_URL}${endpoint}`, {
+ headers: { Authorization: `Bearer ${token}` }
+ });
+
+ const sorted = res.data.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
+ setOrders(sorted);
+ setError(null);
+ } catch(e) {
+ console.error(e);
+ // Ошибки теперь обрабатываем мягче, так как роут один
+ setError("Не удалось загрузить заказы");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ // ==========================================
+ // ЛОГИКА АДМИНА (УПРАВЛЕНИЕ)
+ // ==========================================
+
+ const advanceStatus = async (id, currentStatus) => {
+ let nextStatus = '';
+
+ // Исправленная логика: проверяем все варианты по порядку
+ if (currentStatus === 'placed') nextStatus = 'processing'; // Новый -> В работу
+ else if (currentStatus === 'processing') nextStatus = 'shipped'; // В работе -> В пути
+ else if (currentStatus === 'shipped') nextStatus = 'completed'; // В пути -> Готов
+ else return; // Если статус уже completed или cancelled, ничего не делаем
+
+ if (!window.confirm(`Перевести заказ в статус "${nextStatus.toUpperCase()}"? Клиент получит уведомление.`)) return;
+
+ try {
+ await axios.put(`${API_URL}/admin/orders/${id}`, { status: nextStatus }, {
+ headers: { Authorization: `Bearer ${localStorage.getItem('token')}` }
+ });
+ fetchOrders(); // Обновляем таблицу
+ } catch(e) {
+ console.error(e);
+ alert('Ошибка обновления статуса');
+ }
+ };
+
+ const cancelOrder = async (id) => {
+ if (!window.confirm('ОТМЕНИТЬ ЗАКАЗ?\n\nНажмите ОК для отмены. Статус сменится на CANCELLED.')) return;
+ try {
+ await axios.put(`${API_URL}/admin/orders/${id}`, { status: 'cancelled' }, {
+ headers: { Authorization: `Bearer ${localStorage.getItem('token')}` }
+ });
+ fetchOrders();
+ } catch(e) { alert('Ошибка отмены'); }
+ };
+
+ // --- РЕНДЕР: ПАЙПЛАЙН (Кружочки для Админа) ---
+ const renderStatusPipeline = (status) => {
+ if (status === 'cancelled') {
+ return (
+
+ );
+ }
+
+ const steps = [
+ { key: 'processing', icon: Package, label: 'В работе' },
+ { key: 'shipped', icon: Truck, label: 'В пути' },
+ { key: 'completed', icon: CheckCircle, label: 'Готов' }
+ ];
+
+ // Если статус 'placed', мы на "нулевом" шаге (перед первым)
+ let activeIndex = -1;
+ if (status === 'processing') activeIndex = 0;
+ if (status === 'shipped') activeIndex = 1;
+ if (status === 'completed') activeIndex = 2;
+
+ return (
+
+ {status === 'placed' && (
+
НОВЫЙ
+ )}
+ {steps.map((step, idx) => {
+ const isActive = idx === activeIndex;
+ const isPassed = idx < activeIndex;
+
+ let colorClass = 'text-gray-600 border-gray-700 bg-gray-800/50';
+ if (isActive) colorClass = 'text-[var(--accent-color)] border-[var(--accent-color)] bg-[var(--accent-color)]/10 animate-pulse';
+ if (isPassed) colorClass = 'text-green-500 border-green-500 bg-green-500/10';
+
+ return (
+
+
+
+
+ {idx < steps.length - 1 && (
+
+ )}
+
+ );
+ })}
+
+ );
+ };
+
+ // ==========================================
+ // ЛОГИКА ЮЗЕРА (ОБЫЧНЫЕ БЕЙДЖИ)
+ // ==========================================
+ const renderStatusBadge = (status) => {
+ switch (status) {
+ case 'placed': return НОВЫЙ ;
+ case 'processing': return В РАБОТЕ ;
+ case 'shipped': return ОТПРАВЛЕН ;
+ case 'completed': return ВЫПОЛНЕН ;
+ case 'cancelled': return ОТМЕНЕН ;
+ default: return {status} ;
+ }
+ };
+
+ // ==========================================
+ // РЕНДЕР
+ // ==========================================
+
+ const indexOfLastItem = currentPage * itemsPerPage;
+ const indexOfFirstItem = indexOfLastItem - itemsPerPage;
+ const currentItems = orders.slice(indexOfFirstItem, indexOfLastItem);
+ const totalPages = Math.ceil(orders.length / itemsPerPage);
+
+ return (
+
+
+
+ {/* ЗАГОЛОВОК */}
+
+
+
+ {isAdmin ? `УПРАВЛЕНИЕ ЗАКАЗАМИ (${orders.length})` : `МОИ ЗАКАЗЫ (${orders.length})`}
+
+
+
+
+
+
+
+
+ {error ? (
+
+ ) : (
+ <>
+
+
+
+ Номер
+ {isAdmin ? (
+ // Колонка Клиент (Только админ)
+ Клиент
+ ) : (
+ // Колонка Дата (Только юзер)
+ Дата
+ )}
+ Сумма / Оплата
+ Статус
+ Действия
+
+
+
+ {orders.length === 0 ? (
+ Список заказов пуст
+ ) : currentItems.map((o) => (
+
+
+ {/* ID */}
+ #{o.order_number || o.id}
+
+ {/* КЛИЕНТ (АДМИН) ИЛИ ДАТА (ЮЗЕР) */}
+ {isAdmin ? (
+
+ {o.username || 'ID: ' + o.user_id}
+ {o.user_email}
+ {o.telegram_chat_id && TG Linked }
+
+ ) : (
+
+ {new Date(o.created_at).toLocaleDateString()}
+
+ )}
+
+ {/* ОПЛАТА */}
+
+ {o.total} ₽
+
+ {o.payment_status === 'paid' ? 'ОПЛАЧЕНО' : 'ОЖИДАНИЕ'}
+
+
+
+ {/* СТАТУС (РАЗНЫЙ РЕНДЕР) */}
+
+ {isAdmin ? renderStatusPipeline(o.status) : renderStatusBadge(o.status)}
+
+
+ {/* КНОПКИ */}
+
+
+ {/* Кнопка "Глаз" для всех */}
+ setViewData(o)} title="Детали" className="p-2 rounded hover:bg-[var(--accent-color)] hover:text-black transition-colors text-gray-400">
+
+
+
+ {/* КНОПКИ УПРАВЛЕНИЯ (ТОЛЬКО ДЛЯ АДМИНА) */}
+ {isAdmin && o.status !== 'completed' && o.status !== 'cancelled' && (
+ <>
+ {/* ОТМЕНА */}
+ cancelOrder(o.id)}
+ title="Отменить заказ"
+ className="p-2 rounded text-red-500 hover:bg-red-500 hover:text-white border border-red-500/30 hover:border-red-500 transition-colors"
+ >
+
+
+
+ {/* ВПЕРЕД */}
+ advanceStatus(o.id, o.status)}
+ className="flex items-center gap-1 bg-[var(--accent-color)] text-black px-3 py-1.5 rounded text-[10px] font-bold uppercase tracking-wide hover:bg-white transition-all shadow-lg shadow-[var(--accent-color)]/20"
+ >
+ {o.status === 'placed' ? 'В РАБОТУ' : (o.status === 'processing' ? 'ОТПРАВИТЬ' : 'ЗАВЕРШИТЬ')}
+
+
+ >
+ )}
+
+
+ ))}
+
+
+
+ {/* ПАГИНАЦИЯ */}
+
+ setCurrentPage(p => Math.max(1, p - 1))} disabled={currentPage === 1} className="p-2 disabled:opacity-30 hover:text-[var(--accent-color)]">
+ СТР. {currentPage} / {totalPages}
+ setCurrentPage(p => Math.min(totalPages, p + 1))} disabled={currentPage === totalPages} className="p-2 disabled:opacity-30 hover:text-[var(--accent-color)]">
+
+ >
+ )}
+
+
+
+ {/* МОДАЛКА ПРОСМОТРА (ОБЩАЯ) */}
+ {viewData && (
+
+
+
setViewData(null)} className="absolute top-4 right-4 hover:text-[var(--accent-color)]">
+
+
+
ЗАКАЗ #{viewData.order_number || viewData.id}
+
{new Date(viewData.created_at).toLocaleString()}
+
+ {/* В модалке показываем пайплайн для красоты всем, или бейдж */}
+
+ {isAdmin ? renderStatusPipeline(viewData.status) : renderStatusBadge(viewData.status)}
+
+
+
+
+ {isAdmin && (
+
+
Клиент
+
+ {viewData.username}
+ {viewData.user_email}
+
+
+ )}
+
+
+
Состав заказа:
+
{viewData.content}
+
+
+
+
+ Метод оплаты
+ {viewData.payment_method || 'Карта'}
+
+
+ Статус оплаты
+
+ {viewData.payment_status === 'paid' ? 'ОПЛАЧЕНО' : 'ОЖИДАЕТ'}
+
+
+
+
+
+ Адрес доставки
+ {viewData.delivery_address || 'Нет данных'}
+
+
+
+ ИТОГО:
+ {viewData.total} ₽
+
+
+
+
+ )}
+
+ );
+};
+export default OrdersTable;
diff --git a/src/pages/tables/ProductsTable.jsx b/src/pages/tables/ProductsTable.jsx
new file mode 100644
index 0000000..de10c12
--- /dev/null
+++ b/src/pages/tables/ProductsTable.jsx
@@ -0,0 +1,236 @@
+import React, { useState, useEffect } from 'react';
+import axios from 'axios';
+import { Plus, X, Eye, ChevronLeft, ChevronRight, AlertCircle } from 'lucide-react';
+import ActionMenu from '../../components/ActionMenu';
+
+const API_URL = 'https://diplomnexus.aptcloud.ru';
+
+const ProductsTable = ({ user }) => {
+ const [products, setProducts] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [isModalOpen, setIsModalOpen] = useState(false);
+ const [viewData, setViewData] = useState(null);
+ const [formData, setFormData] = useState({ id: null, name: '', price: '', category: 'General', description: '', image: null });
+
+ // ПАГИНАЦИЯ
+ const [currentPage, setCurrentPage] = useState(1);
+ const itemsPerPage = 10;
+
+ // ОПРЕДЕЛЕНИЕ ПРАВ
+ const isAdmin = user?.role === 'admin' || user?.email === 'admin@mail.ru';
+
+ useEffect(() => {
+ fetchProducts();
+ }, []);
+
+ const fetchProducts = async () => {
+ setLoading(true);
+ try {
+ const res = await axios.get(`${API_URL}/products`);
+ setProducts(res.data);
+ } catch(e) { console.error(e); }
+ finally { setLoading(false); }
+ };
+
+ const handleDelete = async (id) => {
+ if (!isAdmin) return;
+ if(window.confirm('Удалить товар?')) {
+ try {
+ await axios.delete(`${API_URL}/admin/products/${id}`, {
+ headers: { Authorization: `Bearer ${localStorage.getItem('token')}` }
+ });
+ fetchProducts();
+ } catch(e) { alert("Ошибка удаления"); }
+ }
+ };
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ if (!isAdmin) return;
+
+ const data = new FormData();
+ Object.keys(formData).forEach(k => data.append(k, formData[k]));
+
+ const cfg = { headers: { Authorization: `Bearer ${localStorage.getItem('token')}` } };
+ try {
+ if(formData.id) await axios.put(`${API_URL}/admin/products/${formData.id}`, formData, cfg);
+ else await axios.post(`${API_URL}/admin/products`, data, cfg);
+ setIsModalOpen(false);
+ fetchProducts();
+ } catch(e) { alert("Ошибка сохранения"); }
+ };
+
+ // --- ФУНКЦИЯ ДЛЯ ПРАВИЛЬНОГО URL КАРТИНКИ ---
+ const getImageUrl = (img) => {
+ if (!img) return null;
+ if (img.startsWith('http') || img.startsWith('https')) {
+ return img; // Внешняя ссылка
+ }
+ return `${API_URL}/${img}`; // Локальный файл
+ };
+
+ const indexOfLastItem = currentPage * itemsPerPage;
+ const indexOfFirstItem = indexOfLastItem - itemsPerPage;
+ const currentItems = products.slice(indexOfFirstItem, indexOfLastItem);
+ const totalPages = Math.ceil(products.length / itemsPerPage);
+
+ return (
+
+
+
+
+
+ БАЗА ТОВАРОВ ({products.length})
+
+ {/* КНОПКА ДОБАВИТЬ - ТОЛЬКО ДЛЯ АДМИНА */}
+ {isAdmin && (
+
{ setFormData({ id: null, name: '', price: '', category: '', description: '', image: null }); setIsModalOpen(true); }} className="btn-neon px-4 py-2 text-xs font-bold flex gap-2">
+ ДОБАВИТЬ
+
+ )}
+
+
+
+ {loading ? (
+
Загрузка товаров...
+ ) : (
+ <>
+
+
+
+ IMG
+ Название
+ Категория
+ Цена
+ Инфо
+
+
+
+ {currentItems.map((p) => (
+
+
+ {p.image ? (
+
+ ) : (
+ NO
+ )}
+
+ {p.name}
+
+
+ {p.category}
+
+
+ {p.price} ₽
+
+ {/* КНОПКА ПРОСМОТРА ДЛЯ ВСЕХ */}
+ setViewData(p)} className="p-2 text-gray-400 hover:text-[var(--accent-color)] transition-colors">
+
+
+ {/* МЕНЮ РЕДАКТИРОВАНИЯ ТОЛЬКО ДЛЯ АДМИНА */}
+ {isAdmin && (
+ { setFormData(p); setIsModalOpen(true); }}
+ onDelete={() => handleDelete(p.id)}
+ />
+ )}
+
+
+ ))}
+
+
+
+ {/* ПАГИНАЦИЯ */}
+ {totalPages > 1 && (
+
+ setCurrentPage(p => Math.max(1, p - 1))} disabled={currentPage === 1} className="p-2 disabled:opacity-30 hover:text-[var(--accent-color)]">
+ СТР. {currentPage} ИЗ {totalPages}
+ setCurrentPage(p => Math.min(totalPages, p + 1))} disabled={currentPage === totalPages} className="p-2 disabled:opacity-30 hover:text-[var(--accent-color)]">
+
+ )}
+ >
+ )}
+
+
+
+ {/* EDIT MODAL (ONLY ADMIN CAN TRIGGER) */}
+ {isModalOpen && isAdmin && (
+
+ )}
+
+ {/* VIEW MODAL (FOR EVERYONE) */}
+ {viewData && (
+
+
+
setViewData(null)} className="absolute top-2 right-2 z-10 bg-black/50 p-1 rounded-full text-white hover:bg-[var(--accent-color)] hover:text-black transition-colors">
+
+ {/* Левая часть - Картинка */}
+
+ {viewData.image ? (
+
+ ) : (
+
+ )}
+
+
+ {viewData.price} ₽
+
+
+
+
+ {/* Правая часть - Инфо */}
+
+
+
{viewData.name}
+
ID: {viewData.id} | {new Date(viewData.created_at).toLocaleDateString()}
+
+
+
+
+ Категория
+ {viewData.category}
+
+
+ Наличие
+ В наличии
+
+
+
+
+
Описание:
+
+ {viewData.description || 'Описание отсутствует.'}
+
+
+
+
+
+ )}
+
+ );
+};
+
+export default ProductsTable;
diff --git a/src/pages/tables/SensorsTable.jsx b/src/pages/tables/SensorsTable.jsx
new file mode 100644
index 0000000..8a1824d
--- /dev/null
+++ b/src/pages/tables/SensorsTable.jsx
@@ -0,0 +1,262 @@
+import React, { useState, useEffect } from 'react';
+import axios from 'axios';
+import { Plus, X, Eye, ChevronLeft, ChevronRight, AlertCircle, ShieldCheck } from 'lucide-react';
+import ActionMenu from '../../components/ActionMenu';
+
+const API_URL = 'https://diplomnexus.aptcloud.ru';
+
+const SensorsTable = ({ user }) => {
+ const [sensors, setSensors] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [isModalOpen, setIsModalOpen] = useState(false);
+ const [viewData, setViewData] = useState(null);
+ const [formData, setFormData] = useState({ id: null, name: '', price: '', category: 'sensors', description: '', image: null });
+
+ // ПАГИНАЦИЯ
+ const [currentPage, setCurrentPage] = useState(1);
+ const itemsPerPage = 10;
+
+ // ОПРЕДЕЛЕНИЕ ПРАВ
+ const isAdmin = user?.role === 'admin' || user?.email === 'admin@mail.ru';
+
+ useEffect(() => {
+ fetchSensors();
+ }, []);
+
+ const fetchSensors = async () => {
+ setLoading(true);
+ try {
+ // Запрос к специализированной таблице sensors
+ const res = await axios.get(`${API_URL}/api/sensors`);
+ setSensors(res.data);
+ } catch(e) {
+ console.error("Ошибка при загрузке датчиков:", e);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleDelete = async (id) => {
+ if (!isAdmin) return;
+ if(window.confirm('Удалить этот датчик из системы безопасности?')) {
+ try {
+ await axios.delete(`${API_URL}/admin/sensors/${id}`, {
+ headers: { Authorization: `Bearer ${localStorage.getItem('token')}` }
+ });
+ fetchSensors();
+ } catch(e) { alert("Ошибка удаления"); }
+ }
+ };
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ if (!isAdmin) return;
+
+ const data = new FormData();
+ Object.keys(formData).forEach(k => {
+ if (formData[k] !== null) data.append(k, formData[k]);
+ });
+
+ const cfg = { headers: { Authorization: `Bearer ${localStorage.getItem('token')}` } };
+ try {
+ if(formData.id) {
+ await axios.put(`${API_URL}/admin/sensors/${formData.id}`, formData, cfg);
+ } else {
+ await axios.post(`${API_URL}/admin/sensors`, data, cfg);
+ }
+ setIsModalOpen(false);
+ fetchSensors();
+ } catch(e) { alert("Ошибка сохранения"); }
+ };
+
+ const getImageUrl = (img) => {
+ if (!img) return null;
+ if (img.startsWith('http')) return img;
+ return `${API_URL}/${img}`;
+ };
+
+ const indexOfLastItem = currentPage * itemsPerPage;
+ const indexOfFirstItem = indexOfLastItem - itemsPerPage;
+ const currentItems = sensors.slice(indexOfFirstItem, indexOfLastItem);
+ const totalPages = Math.ceil(sensors.length / itemsPerPage);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ ДАТЧИКИ И СЕНСОРЫ
+
+
Периметр безопасности и мониторинг среды ({sensors.length} шт.)
+
+
+ {isAdmin && (
+
{ setFormData({ id: null, name: '', price: '', category: 'sensors', description: '', image: null }); setIsModalOpen(true); }}
+ className="btn-neon px-4 py-2 text-xs font-bold flex gap-2"
+ >
+ НОВЫЙ СЕНСОР
+
+ )}
+
+
+
+ {loading ? (
+
CHECKING_SENSORS...
+ ) : (
+ <>
+
+
+
+ SCAN
+ Тип датчика
+ SKU / Протокол
+ Цена
+ Управление
+
+
+
+ {currentItems.map((p) => (
+
+
+ {p.image ? (
+
+ ) : (
+ OFFLINE
+ )}
+
+ {p.name}
+ {p.sku || `SENS-${p.id}`}
+ {p.price} ₽
+
+ setViewData(p)} className="p-2 text-gray-400 hover:text-[var(--accent-color)] transition-colors">
+
+
+ {isAdmin && (
+ { setFormData({...p, category: 'sensors'}); setIsModalOpen(true); }}
+ onDelete={() => handleDelete(p.id)}
+ />
+ )}
+
+
+ ))}
+
+
+
+ {totalPages > 1 && (
+
+ setCurrentPage(p => Math.max(1, p - 1))} disabled={currentPage === 1} className="p-2 disabled:opacity-30 hover:text-[var(--accent-color)]">
+ NODE {currentPage} / {totalPages}
+ setCurrentPage(p => Math.min(totalPages, p + 1))} disabled={currentPage === totalPages} className="p-2 disabled:opacity-30 hover:text-[var(--accent-color)]">
+
+ )}
+ >
+ )}
+
+
+
+ {/* MODAL EDIT/CREATE */}
+ {isModalOpen && isAdmin && (
+
+ )}
+
+ {/* VIEW MODAL */}
+ {viewData && (
+
+
+
setViewData(null)} className="absolute top-2 right-2 z-10 bg-black/50 p-1 rounded-full text-white hover:bg-[var(--accent-color)] hover:text-black transition-colors">
+
+
+ {viewData.image ? (
+
+ ) : (
+
+ )}
+
+
+
+ {viewData.price} ₽
+
+
+
+
+
+
+
{viewData.name}
+
NODE_ID: {viewData.id} / {viewData.sku || 'ZIGBEE_NODE'}
+
+
+
+
+ Питание
+ CR2450 / 2 года
+
+
+ Сигнал
+ Zigbee / Wi-Fi
+
+
+
+
+
Сведения о сенсоре:
+
+ {viewData.description || 'Данные о чувствительности и радиусе действия отсутствуют.'}
+
+
+
+
+
+ )}
+
+ );
+};
+
+export default SensorsTable;
\ No newline at end of file
diff --git a/src/pages/tables/UsersTable.jsx b/src/pages/tables/UsersTable.jsx
new file mode 100644
index 0000000..99b8b63
--- /dev/null
+++ b/src/pages/tables/UsersTable.jsx
@@ -0,0 +1,237 @@
+import React, { useState, useEffect } from 'react';
+import axios from 'axios';
+import { Shield, User, Plus, X, Eye, ChevronLeft, ChevronRight, AlertCircle } from 'lucide-react';
+import ActionMenu from '../../components/ActionMenu';
+
+const API_URL = 'https://diplomnexus.aptcloud.ru';
+
+const UsersTable = ({ user }) => {
+ const [users, setUsers] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+ const [isModalOpen, setIsModalOpen] = useState(false);
+ const [viewData, setViewData] = useState(null);
+ const [formData, setFormData] = useState({ id: null, name: '', email: '', password: '', role: 'user', status: 'active' });
+
+ // ПАГИНАЦИЯ
+ const [currentPage, setCurrentPage] = useState(1);
+ const itemsPerPage = 10;
+
+ // Проверка прав (админ видит кнопки управления, обычный юзер - только список)
+ const isAdmin = user?.role === 'admin' || user?.email === 'admin@mail.ru';
+
+ useEffect(() => {
+ fetchUsers();
+ }, []);
+
+ const fetchUsers = async () => {
+ setLoading(true);
+ try {
+ // !!! ГЛАВНОЕ ИСПРАВЛЕНИЕ: ДОБАВЛЕН ЗАГОЛОВОК С ТОКЕНОМ !!!
+ const token = localStorage.getItem('token');
+ const res = await axios.get(`${API_URL}/users`, {
+ headers: { Authorization: `Bearer ${token}` }
+ });
+ setUsers(res.data);
+ setError(null);
+ } catch (e) {
+ console.error("Ошибка загрузки пользователей:", e);
+ setError("Не удалось загрузить список.");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleDelete = async (id) => {
+ if (!isAdmin) return alert("Только для админов");
+ if (window.confirm('Уничтожить пользователя?')) {
+ try {
+ await axios.delete(`${API_URL}/admin/users/${id}`, { headers: { Authorization: `Bearer ${localStorage.getItem('token')}` } });
+ fetchUsers();
+ } catch(e) { alert("Ошибка удаления"); }
+ }
+ };
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ const token = localStorage.getItem('token');
+ const headers = { Authorization: `Bearer ${token}` };
+ try {
+ if (formData.id) await axios.put(`${API_URL}/admin/users/${formData.id}`, formData, { headers });
+ else await axios.post(`${API_URL}/admin/users`, formData, { headers });
+ setIsModalOpen(false);
+ fetchUsers();
+ } catch (e) { alert('Ошибка сохранения'); }
+ };
+
+ // ЛОГИКА ПАГИНАЦИИ
+ const indexOfLastItem = currentPage * itemsPerPage;
+ const indexOfFirstItem = indexOfLastItem - itemsPerPage;
+ const currentItems = users.slice(indexOfFirstItem, indexOfLastItem);
+ const totalPages = Math.ceil(users.length / itemsPerPage);
+
+ return (
+
+
+
+
+
ПОЛЬЗОВАТЕЛИ({users.length})
+
+
+ {/* Кнопка "Создать" только для админа */}
+ {isAdmin && (
+
{ setFormData({ id: null, name: '', email: '', password: '', role: 'user', status: 'active' }); setIsModalOpen(true); }}
+ className="btn-neon px-4 py-2 text-xs font-bold flex items-center gap-2"
+ >
+ НОВЫЙ
+
+ )}
+
+
+
+ {loading ? (
+
Загрузка списка пользователей...
+ ) : error ? (
+
+ ) : (
+ <>
+
+
+
+ ID
+ Имя
+ Роль
+ Статус
+ Действия
+
+
+
+ {currentItems.map((u) => (
+
+ #{u.id}
+
+
+ {u.role==='admin'?:}
+
+
+ {u.name}
+ {u.email}
+
+
+ {u.role}
+
+
+ {u.status}
+
+
+
+ setViewData(u)} className="p-2 text-gray-400 hover:text-[var(--accent-color)] transition-colors">
+
+
+ {/* Меню действий только для админа */}
+ {isAdmin && (
+ { setFormData({...u, password: ''}); setIsModalOpen(true); }}
+ onDelete={() => handleDelete(u.id)}
+ />
+ )}
+
+
+ ))}
+
+
+
+ {/* PAGINATION CONTROLS */}
+ {users.length > 0 && (
+
+ setCurrentPage(p => Math.max(1, p - 1))} disabled={currentPage === 1} className="p-2 disabled:opacity-30 hover:text-[var(--accent-color)]">
+ СТРАНИЦА {currentPage} ИЗ {totalPages}
+ setCurrentPage(p => Math.min(totalPages, p + 1))} disabled={currentPage === totalPages} className="p-2 disabled:opacity-30 hover:text-[var(--accent-color)]">
+
+ )}
+ >
+ )}
+
+
+
+ {/* MODAL CREATE/EDIT */}
+ {isModalOpen && (
+
+ )}
+
+ {/* MODAL VIEW DETAILS (Обновленный красивый вид) */}
+ {viewData && (
+
+
+
setViewData(null)} className="absolute top-4 right-4 hover:text-[var(--accent-color)]">
+
+
+
+ {viewData.name ? viewData.name[0].toUpperCase() : '?'}
+
+
+
{viewData.name}
+
{viewData.role}
+
+
+
+
+
+ Email
+ {viewData.email}
+
+
+ Телефон
+ {viewData.phone || 'Не указан'}
+
+
+ Статус
+ {viewData.status}
+
+
+ IP Адрес
+ {viewData.ip || 'Нет данных'}
+
+
+ Referral Code
+ {viewData.referral_code}
+
+
+ Последний вход
+ {viewData.last_login ? new Date(viewData.last_login).toLocaleDateString() : '-'}
+
+
+ Дата регистрации
+ {viewData.created_at ? new Date(viewData.created_at).toLocaleString() : '-'}
+
+
+
+
+ )}
+
+ );
+};
+
+export default UsersTable;
\ No newline at end of file
diff --git a/src/styles/admin.css b/src/styles/admin.css
new file mode 100644
index 0000000..8defc03
--- /dev/null
+++ b/src/styles/admin.css
@@ -0,0 +1,62 @@
+/* Оформление таблиц админки */
+.admin-container {
+ @apply max-w-7xl mx-auto pb-10;
+}
+
+.admin-header {
+ @apply text-4xl font-bold mb-8 bg-clip-text text-transparent bg-gradient-to-r from-[var(--accent-color)] to-purple-500;
+ filter: drop-shadow(0 0 10px rgba(56, 189, 248, 0.2));
+}
+
+.admin-table-wrapper {
+ @apply rounded-xl overflow-hidden shadow-2xl;
+ background: var(--card-bg);
+ border: 1px solid var(--glass-border);
+}
+
+.admin-table {
+ @apply w-full text-left border-collapse;
+}
+
+.admin-table thead {
+ background: rgba(0, 0, 0, 0.2);
+ color: var(--accent-color);
+ text-transform: uppercase;
+ font-size: 0.85rem;
+ letter-spacing: 0.05em;
+}
+
+.admin-table th, .admin-table td {
+ @apply p-4 border-b border-[var(--glass-border)];
+}
+
+.admin-table tbody tr:hover {
+ background: rgba(56, 189, 248, 0.05);
+}
+
+.status-badge {
+ @apply px-3 py-1 rounded-full text-xs font-bold uppercase;
+}
+.status-new { @apply bg-blue-500/20 text-blue-400; }
+.status-completed { @apply bg-green-500/20 text-green-400; }
+.status-in-progress { @apply bg-yellow-500/20 text-yellow-400; }
+.status-canceled { @apply bg-red-500/20 text-red-400; }
+
+.admin-grid-menu {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
+ gap: 2rem;
+}
+
+.admin-card {
+ @apply p-6 rounded-2xl flex flex-col items-center justify-center gap-4 cursor-pointer transition-all duration-300;
+ background: var(--card-bg);
+ border: 1px solid var(--glass-border);
+ height: 200px;
+}
+
+.admin-card:hover {
+ transform: translateY(-5px);
+ border-color: var(--accent-color);
+ box-shadow: 0 10px 30px -10px var(--accent-color);
+}
\ No newline at end of file
diff --git a/src/styles/pages.css b/src/styles/pages.css
new file mode 100644
index 0000000..799664b
--- /dev/null
+++ b/src/styles/pages.css
@@ -0,0 +1,64 @@
+/* src/styles/pages.css */
+
+/* --- Типографика (Неон) --- */
+.neon-title {
+ @apply text-5xl md:text-7xl font-bold mb-6 text-center;
+ background: linear-gradient(90deg, #fff, #00f3ff, #bc13fe);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ filter: drop-shadow(0 0 20px rgba(188, 19, 254, 0.4));
+}
+
+.text-gradient {
+ @apply bg-clip-text text-transparent bg-gradient-to-r from-neon-blue to-white;
+}
+
+/* --- Стеклянные панели --- */
+.glass-panel {
+ @apply backdrop-blur-xl border border-white/10 rounded-2xl p-8 transition-all duration-300;
+ background: rgba(255, 255, 255, 0.03);
+ box-shadow: 0 4px 30px rgba(0, 0, 0, 0.5);
+}
+
+.glass-panel:hover {
+ border-color: rgba(0, 243, 255, 0.3);
+ box-shadow: 0 0 30px rgba(0, 243, 255, 0.15);
+ transform: translateY(-5px);
+}
+
+/* --- Hero секция --- */
+.hero-img {
+ @apply w-full h-full object-cover transition-transform duration-700 hover:scale-105;
+}
+
+/* --- Грид фич --- */
+.feature-grid {
+ @apply grid grid-cols-1 md:grid-cols-3 gap-8 py-10 px-4 max-w-7xl mx-auto relative z-10;
+}
+
+.feature-icon {
+ @apply w-16 h-16 rounded-full flex items-center justify-center mb-6 mx-auto;
+ background: rgba(0, 243, 255, 0.1);
+ border: 1px solid rgba(0, 243, 255, 0.2);
+ box-shadow: 0 0 15px rgba(0, 243, 255, 0.2);
+}
+
+/* --- Карта --- */
+.map-frame {
+ @apply w-full h-[400px] rounded-xl overflow-hidden border border-white/10 relative;
+ filter: grayscale(100%) invert(100%) contrast(1.2);
+ transition: filter 0.5s ease;
+}
+
+.map-frame:hover {
+ filter: grayscale(0%) invert(0%);
+}
+
+/* --- Инпуты контактов --- */
+.contact-input {
+ @apply w-full bg-black/40 border border-white/10 rounded-lg p-4 text-white outline-none transition-all duration-300;
+}
+
+.contact-input:focus {
+ @apply border-neon-blue shadow-[0_0_15px_rgba(0,243,255,0.3)];
+}
\ No newline at end of file
diff --git a/tailwind.config.js b/tailwind.config.js
index 89a305e..040340d 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -5,7 +5,19 @@ export default {
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
- extend: {},
+ extend: {
+ colors: {
+ neon: {
+ blue: '#00f3ff',
+ purple: '#bc13fe',
+ dark: '#0a0a12',
+ surface: '#13131f'
+ }
+ },
+ fontFamily: {
+ sans: ['Inter', 'sans-serif'],
+ }
+ },
},
plugins: [],
}
\ No newline at end of file
diff --git a/vite.config.js b/vite.config.js
index 8b0f57b..77af5da 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -1,7 +1,11 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
-// https://vite.dev/config/
+// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
-})
+ server: {
+ port: 3000, // <--- СТАВИМ ПОРТ 3000
+ host: true
+ }
+})
\ No newline at end of file
diff --git a/vk-hosting-config.json b/vk-hosting-config.json
new file mode 100644
index 0000000..5074e31
--- /dev/null
+++ b/vk-hosting-config.json
@@ -0,0 +1,9 @@
+{
+ "static_path": "dist",
+ "app_id": 54612192,
+ "endpoints": {
+ "mobile": "index.html",
+ "mvk": "index.html",
+ "web": "index.html"
+ }
+}
\ No newline at end of file