diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..e74df74 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,18 @@ +{ + "root": true, + "extends": [ + "plugin:@typescript-eslint/recommended", + "prettier" + ], + "parser": "@typescript-eslint/parser", + "plugins": [ + "@typescript-eslint", + "prettier" + ], + "rules": { + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-empty-object-type": "off", + "@typescript-eslint/no-unused-vars": "off", + "prettier/prettier": "off" + } +} diff --git a/.github/workflows/manual.yml b/.github/workflows/manual.yml new file mode 100644 index 0000000..90402fc --- /dev/null +++ b/.github/workflows/manual.yml @@ -0,0 +1,48 @@ +name: Kotyari Build + +on: + push: + branches: [main] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Install Node + uses: actions/setup-node@v4 + with: + node-version: 16 + + - name: Checkout code + run: actions/checkout@v4 + + - name: Install deps + run: npm ci + + - name: Build + run: npm run build + + - name: Upload build result + uses: actions/upload-artifact@v1 + with: + name: dist + path: ./dist + + deploy: + runs-on: ubuntu-latest + + steps: + - name: Download build + uses: actions/download-artifact@v1 + with: + name: dist + - name: Transfer build files to server + uses: appleboy/scp-action@master + with: + host: 94.139.246.241 + username: ubuntu + key: ${{ secrets.PRIVATE_KEY }} + source: "dist/*" + target: "/home/ubuntu/dist" + strip_components: 1 diff --git a/.gitignore b/.gitignore index 9113e7a..96b006f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +key-yandex.ts ./dist/ node_modules diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000..ab10b9b --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +# .husky/pre-commit + +prettier $(git diff --cached --name-only --diff-filter=ACMR | sed 's| |\\ |g') --write --ignore-unknown +git update-index --again diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..04c01ba --- /dev/null +++ b/.prettierignore @@ -0,0 +1,2 @@ +node_modules/ +dist/ \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..4f6b534 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,9 @@ +{ + "trailingComma": "all", + "tabWidth": 2, + "semi": true, + "singleQuote": true, + "printWidth": 120, + "bracketSpacing": true, + "endOfLine": "lf" +} diff --git a/eslint.config.js b/eslint.config.js deleted file mode 100644 index 719b029..0000000 --- a/eslint.config.js +++ /dev/null @@ -1,4 +0,0 @@ -import globals from 'globals'; -import pluginJs from '@eslint/js'; - -export default [{ languageOptions: { globals: globals.browser } }, pluginJs.configs.recommended]; diff --git a/eslint.config.mjs b/eslint.config.mjs deleted file mode 100644 index e363415..0000000 --- a/eslint.config.mjs +++ /dev/null @@ -1,27 +0,0 @@ -import globals from "globals"; -import pluginJs from "@eslint/js"; -import prettier from 'eslint-plugin-prettier'; - -export default [ - { - languageOptions: { - globals: { - ...globals.browser, - Handlebars: 'readonly', - }, - }, - - plugins: { - prettier, - }, - rules: { - 'prettier/prettier': 'error', - 'semi': ['warn', 'always'], - }, - files: ['**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx'], - ignores: [ - 'node_modules/**', - ], - }, - pluginJs.configs.recommended, -]; diff --git a/package-lock.json b/package-lock.json index 7b5dc53..d2c17bd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,9 @@ }, "devDependencies": { "@types/handlebars": "^4.0.40", - "@typescript-eslint/eslint-plugin": "^8.10.0", + "@types/node": "^22.10.2", + "@typescript-eslint/eslint-plugin": "^8.18.0", + "@typescript-eslint/parser": "^8.18.0", "eslint": "^8.0.1", "eslint-config-prettier": "^9.1.0", "eslint-config-standard": "^17.1.0", @@ -24,7 +26,8 @@ "lint-staged": "^15.2.10", "prettier": "^3.3.3", "sass": "^1.81.0", - "vite": "^5.4.9", + "typescript": "^5.7.2", + "vite": "^5.4.11", "vite-plugin-handlebars": "^2.0.0", "vite-plugin-pwa": "^0.20.5", "vite-plugin-string": "^1.2.3" @@ -3026,6 +3029,16 @@ "license": "MIT", "peer": true }, + "node_modules/@types/node": { + "version": "22.10.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz", + "integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, "node_modules/@types/resolve": { "version": "1.20.2", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", @@ -3041,17 +3054,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.12.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.12.2.tgz", - "integrity": "sha512-gQxbxM8mcxBwaEmWdtLCIGLfixBMHhQjBqR8sVWNTPpcj45WlYL2IObS/DNMLH1DBP0n8qz+aiiLTGfopPEebw==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.18.0.tgz", + "integrity": "sha512-NR2yS7qUqCL7AIxdJUQf2MKKNDVNaig/dEB0GBLU7D+ZdHgK1NoH/3wsgO3OnPVipn51tG3MAwaODEGil70WEw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.12.2", - "@typescript-eslint/type-utils": "8.12.2", - "@typescript-eslint/utils": "8.12.2", - "@typescript-eslint/visitor-keys": "8.12.2", + "@typescript-eslint/scope-manager": "8.18.0", + "@typescript-eslint/type-utils": "8.18.0", + "@typescript-eslint/utils": "8.18.0", + "@typescript-eslint/visitor-keys": "8.18.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -3066,26 +3079,21 @@ }, "peerDependencies": { "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", - "eslint": "^8.57.0 || ^9.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.12.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.12.2.tgz", - "integrity": "sha512-MrvlXNfGPLH3Z+r7Tk+Z5moZAc0dzdVjTgUgwsdGweH7lydysQsnSww3nAmsq8blFuRD5VRlAr9YdEFw3e6PBw==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.18.0.tgz", + "integrity": "sha512-hgUZ3kTEpVzKaK3uNibExUYm6SKKOmTU2BOxBSvOYwtJEPdVQ70kZJpPjstlnhCHcuc2WGfSbpKlb/69ttyN5Q==", "dev": true, - "license": "BSD-2-Clause", - "peer": true, + "license": "MITClause", "dependencies": { - "@typescript-eslint/scope-manager": "8.12.2", - "@typescript-eslint/types": "8.12.2", - "@typescript-eslint/typescript-estree": "8.12.2", - "@typescript-eslint/visitor-keys": "8.12.2", + "@typescript-eslint/scope-manager": "8.18.0", + "@typescript-eslint/types": "8.18.0", + "@typescript-eslint/typescript-estree": "8.18.0", + "@typescript-eslint/visitor-keys": "8.18.0", "debug": "^4.3.4" }, "engines": { @@ -3096,23 +3104,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.12.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.12.2.tgz", - "integrity": "sha512-gPLpLtrj9aMHOvxJkSbDBmbRuYdtiEbnvO25bCMza3DhMjTQw0u7Y1M+YR5JPbMsXXnSPuCf5hfq0nEkQDL/JQ==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.18.0.tgz", + "integrity": "sha512-PNGcHop0jkK2WVYGotk/hxj+UFLhXtGPiGtiaWgVBVP1jhMoMCHlTyJA+hEj4rszoSdLTK3fN4oOatrL0Cp+Xw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.12.2", - "@typescript-eslint/visitor-keys": "8.12.2" + "@typescript-eslint/types": "8.18.0", + "@typescript-eslint/visitor-keys": "8.18.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3123,14 +3127,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.12.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.12.2.tgz", - "integrity": "sha512-bwuU4TAogPI+1q/IJSKuD4shBLc/d2vGcRT588q+jzayQyjVK2X6v/fbR4InY2U2sgf8MEvVCqEWUzYzgBNcGQ==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.18.0.tgz", + "integrity": "sha512-er224jRepVAVLnMF2Q7MZJCq5CsdH2oqjP4dT7K6ij09Kyd+R21r7UVJrF0buMVdZS5QRhDzpvzAxHxabQadow==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.12.2", - "@typescript-eslint/utils": "8.12.2", + "@typescript-eslint/typescript-estree": "8.18.0", + "@typescript-eslint/utils": "8.18.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -3141,16 +3145,15 @@ "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.12.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.12.2.tgz", - "integrity": "sha512-VwDwMF1SZ7wPBUZwmMdnDJ6sIFk4K4s+ALKLP6aIQsISkPv8jhiw65sAK6SuWODN/ix+m+HgbYDkH+zLjrzvOA==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.18.0.tgz", + "integrity": "sha512-FNYxgyTCAnFwTrzpBGq+zrnoTO4x0c1CKYY5MuUTzpScqmY5fmsh2o3+57lqdI3NZucBDCzDgdEbIaNfAjAHQA==", "dev": true, "license": "MIT", "engines": { @@ -3162,14 +3165,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.12.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.12.2.tgz", - "integrity": "sha512-mME5MDwGe30Pq9zKPvyduyU86PH7aixwqYR2grTglAdB+AN8xXQ1vFGpYaUSJ5o5P/5znsSBeNcs5g5/2aQwow==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.18.0.tgz", + "integrity": "sha512-rqQgFRu6yPkauz+ms3nQpohwejS8bvgbPyIDq13cgEDbkXt4LH4OkDMT0/fN1RUtzG8e8AKJyDBoocuQh8qNeg==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.12.2", - "@typescript-eslint/visitor-keys": "8.12.2", + "@typescript-eslint/types": "8.18.0", + "@typescript-eslint/visitor-keys": "8.18.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -3184,23 +3187,21 @@ "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "peerDependencies": { + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/@typescript-eslint/utils": { - "version": "8.12.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.12.2.tgz", - "integrity": "sha512-UTTuDIX3fkfAz6iSVa5rTuSfWIYZ6ATtEocQ/umkRSyC9O919lbZ8dcH7mysshrCdrAM03skJOEYaBugxN+M6A==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.18.0.tgz", + "integrity": "sha512-p6GLdY383i7h5b0Qrfbix3Vc3+J2k6QWw6UMUeY5JGfm3C5LbZ4QIZzJNoNOfgyRe0uuYKjvVOsO/jD4SJO+xg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.12.2", - "@typescript-eslint/types": "8.12.2", - "@typescript-eslint/typescript-estree": "8.12.2" + "@typescript-eslint/scope-manager": "8.18.0", + "@typescript-eslint/types": "8.18.0", + "@typescript-eslint/typescript-estree": "8.18.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3210,18 +3211,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0" + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.12.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.12.2.tgz", - "integrity": "sha512-PChz8UaKQAVNHghsHcPyx1OMHoFRUEA7rJSK/mDhdq85bk+PLsUHUBqTQTFt18VJZbmxBovM65fezlheQRsSDA==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.18.0.tgz", + "integrity": "sha512-pCh/qEA8Lb1wVIqNvBke8UaRjJ6wrAWkJO5yyIbs8Yx6TNGYyfNjOo61tLv+WwLvoLPp4BQ8B7AHKijl8NGUfw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.12.2", - "eslint-visitor-keys": "^3.4.3" + "@typescript-eslint/types": "8.18.0", + "eslint-visitor-keys": "^4.2.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3231,6 +3233,19 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@ungap/structured-clone": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", @@ -8470,12 +8485,11 @@ } }, "node_modules/typescript": { - "version": "5.6.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", - "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", + "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -8513,6 +8527,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", @@ -8660,11 +8681,10 @@ } }, "node_modules/vite": { - "version": "5.4.10", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.10.tgz", - "integrity": "sha512-1hvaPshuPUtxeQ0hsVH3Mud0ZanOLwVTneA1EgbAM5LhaZEqyPWGRQ7BtaMvUrTDeEaC8pxtj6a6jku3x4z6SQ==", + "version": "5.4.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz", + "integrity": "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==", "dev": true, - "license": "MIT", "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", diff --git a/package.json b/package.json index 7132edc..30abc7e 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,9 @@ "version": "1.0.0", "devDependencies": { "@types/handlebars": "^4.0.40", - "@typescript-eslint/eslint-plugin": "^8.10.0", + "@types/node": "^22.10.2", + "@typescript-eslint/eslint-plugin": "^8.18.0", + "@typescript-eslint/parser": "^8.18.0", "eslint": "^8.0.1", "eslint-config-prettier": "^9.1.0", "eslint-config-standard": "^17.1.0", @@ -13,7 +15,8 @@ "lint-staged": "^15.2.10", "prettier": "^3.3.3", "sass": "^1.81.0", - "vite": "^5.4.9", + "typescript": "^5.7.2", + "vite": "^5.4.11", "vite-plugin-handlebars": "^2.0.0", "vite-plugin-pwa": "^0.20.5", "vite-plugin-string": "^1.2.3" @@ -28,12 +31,13 @@ "server": "node server/server.js", "prepare": "husky", "format": "prettier --write '**/*.{js,jsx,ts,tsx}'", - "lint": "eslint '**/*.{js,jsx,ts,tsx}'", - "lint:fix": "npm run format && npm run lint -- --fix", + "lint": "eslint . --ext .js,.jsx,.ts,.tsx", + "lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix", "dev": "vite", "build": "vite build", "start": "vite preview", - "scss": "sass public/index.scss public/index.css" + "scss": "sass public/index.scss public/index.css", + "precommit": "lint-staged" }, "husky": { "hooks": { @@ -41,9 +45,10 @@ } }, "lint-staged": { - "*.{js,jsx,ts,tsx,json,css,scss}": [ + "*.{js,ts,tsx}": [ "prettier --write", - "eslint --fix" + "eslint --fix", + "git add" ] } } diff --git a/public/assets/img/static/crying-man.svg b/public/assets/img/static/crying-man.svg new file mode 100644 index 0000000..a338f39 --- /dev/null +++ b/public/assets/img/static/crying-man.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/css/parametrs/basic-mixins.scss b/public/css/parametrs/basic-mixins.scss index 05df585..de001f3 100644 --- a/public/css/parametrs/basic-mixins.scss +++ b/public/css/parametrs/basic-mixins.scss @@ -44,15 +44,16 @@ font-size: 20px; font-weight: bold; color: white; - border: 2px solid #7a16d5; + border: 2px solid rgba(122, 22, 213, 0); background-color: #7a16d5; border-radius: parameters.$default-border-radius; cursor: pointer; - transition: transform 0.2s ease, margin 0.2s ease, background-color 0.3s ease; + transition: transform 0.2s ease, margin 0.2s ease, background-color 0.3s ease, border-color 0.3s ease; &:hover { background-color: #5e11b0; - transform: scale(1.05); /* Увеличение кнопки */ + border-color: #5e11b0; + transform: scale(1.1); } } @@ -126,3 +127,38 @@ } } } + +@mixin placeholder() { + &__placeholder { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 30px; + text-align: center; + font-size: 20px; + font-weight: bold; + color: parameters.$default-text-color; + background-color: #f9f9f9; + border-radius: 12px; + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); + animation: fadeIn 1s ease-in-out; + } + + &__icon { + margin-bottom: 15px; + opacity: 0.7; + transition: transform 0.3s ease; + } + + &__placeholder-text, + &__placeholder-suggestion { + font-size: 18px; + color: #555; + } + + &__placeholder:hover .cart-item__icon { + transform: scale(1.1); + opacity: 1; + } +} diff --git a/public/css/parametrs/parameters.scss b/public/css/parametrs/parameters.scss index e43d976..e9472e0 100644 --- a/public/css/parametrs/parameters.scss +++ b/public/css/parametrs/parameters.scss @@ -283,7 +283,7 @@ $catalog-justify-content: center; $card-justify-content: space-between; $card-allign-items: center; $card-text-align: center; -$card-z-index_on: 5000; +$card-z-index_on: 4000; $card-z-index: 1; $header-z-index: 333; $before-card-z-index: -1; diff --git a/public/index.css b/public/index.css index e69de29..29c01ab 100644 --- a/public/index.css +++ b/public/index.css @@ -0,0 +1,17 @@ +/* Error: Can't find stylesheet to import. + * , + * 2 | @use 'src/scripts/components/dropdown-btn/view/dropdown.scss'; + * | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + * ' + * public\index.scss 2:1 root stylesheet */ + +body::before { + font-family: "Source Code Pro", "SF Mono", Monaco, Inconsolata, "Fira Mono", + "Droid Sans Mono", monospace, monospace; + white-space: pre; + display: block; + padding: 1em; + margin-bottom: 1em; + border-bottom: 2px solid black; + content: "Error: Can't find stylesheet to import.\a \2577 \a 2 \2502 @use 'src/scripts/components/dropdown-btn/view/dropdown.scss';\d\a \2502 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\a \2575 \a public\\index.scss 2:1 root stylesheet"; +} diff --git a/public/index.scss b/public/index.scss index e32f2e3..eb2f357 100644 --- a/public/index.scss +++ b/public/index.scss @@ -24,6 +24,12 @@ @use "src/scripts/components/personal-account/views/personal-account__mobile"; @use 'src/scripts/components/personal-account/views/personal-account__middle'; @use "src/scripts/layouts/tab-bar.scss"; +@use "src/scripts/components/searcher/view/suggestions.scss"; +@use 'src/scripts/components/wish-list/presenter/wish-list'; +@use 'src/scripts/components/wish-list/presenter/all-wishlists'; +@use '../src/scripts/components/wish-list/presenter/wishlist-modal'; +@use "src/scripts/components/notice/view/csat.scss"; + @font-face { font-family: 'UbuntuSans'; src: url('/public/assets/fonts/UbuntuSans.ttf'); diff --git a/server/server.js b/server/server.js index 4795dbe..f2a76f6 100644 --- a/server/server.js +++ b/server/server.js @@ -4,7 +4,7 @@ import { fileURLToPath } from 'url'; import path from 'path'; const app = express(); -const PORT = 3000; +const PORT = 3001; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -101,5 +101,5 @@ app.get('/', (req, res) => { * @param {number} PORT - Порт, на котором будет запущен сервер */ app.listen(PORT, () => { - console.log(`Server is running on port ${PORT}`); + //// console.log(`Server is running on port ${PORT}`); }); diff --git a/src/scripts/components/auth-menu/api/auth.ts b/src/scripts/components/auth-menu/api/auth.ts index 957cfd1..275ad8e 100644 --- a/src/scripts/components/auth-menu/api/auth.ts +++ b/src/scripts/components/auth-menu/api/auth.ts @@ -48,7 +48,7 @@ export default class AuthAPI { throw Error(`ошибка сервера ${res.status} - ${res.body.error_message}`); }) .catch((err) => { - console.error(err); + //// console.error(err); return false; }); }; diff --git a/src/scripts/components/auth-menu/presenters/login.ts b/src/scripts/components/auth-menu/presenters/login.ts index 75f7ab9..bd778c9 100644 --- a/src/scripts/components/auth-menu/presenters/login.ts +++ b/src/scripts/components/auth-menu/presenters/login.ts @@ -55,9 +55,9 @@ export class LoginPresenter { this.router.clearHistory(); this.router.navigate('/'); }) - .catch((err) => { - console.error(err); - }); + // .catch((err) => { + // // console.error(err); + // }); }; private handleLogin = (event: SubmitEvent) => { @@ -90,12 +90,12 @@ export class LoginPresenter { } const error = response.body as ErrorResponse; - console.error('Login error:', error.error_message); + //// console.error('Login error:', error.error_message); this.errorView.displayBackError(error.error_message ?? 'неизвестная ошибка'); }) .catch((error: authResponse) => { const errorBody = error.body as ErrorResponse; - console.error('Ошибка при логине:', errorBody.error_message); + //// console.error('Ошибка при логине:', errorBody.error_message); this.errorView.displayBackError(errorBody.error_message); }); }; diff --git a/src/scripts/components/auth-menu/presenters/signup.ts b/src/scripts/components/auth-menu/presenters/signup.ts index b9ccab2..c41b043 100644 --- a/src/scripts/components/auth-menu/presenters/signup.ts +++ b/src/scripts/components/auth-menu/presenters/signup.ts @@ -43,7 +43,7 @@ export class SignUpPresenter { if (signUpForm) { signUpForm.addEventListener('submit', this.handleSignUp); } else { - console.warn('Форма для регистрации не найдена!'); + //// console.warn('Форма для регистрации не найдена!'); } }; @@ -84,12 +84,12 @@ export class SignUpPresenter { } const error = response.body as ErrorResponse; - console.error('Login error:', error.error_message); + //// console.error('Login error:', error.error_message); this.errorView.displayBackError(error.error_message ?? 'неизвестная ошибка'); }) .catch((error: authResponse) => { const errorBody = error.body as ErrorResponse; - console.error('Ошибка при логине:', errorBody.error_message); + //// console.error('Ошибка при логине:', errorBody.error_message); this.errorView.displayBackError(errorBody.error_message); }); }; diff --git a/src/scripts/components/auth-menu/types/mixins.scss b/src/scripts/components/auth-menu/types/mixins.scss index 728178c..58d7496 100644 --- a/src/scripts/components/auth-menu/types/mixins.scss +++ b/src/scripts/components/auth-menu/types/mixins.scss @@ -6,10 +6,9 @@ border-radius: $border-radius; font-size: $font-size; padding: $padding; - border-color: parameters.$darkest-background-color; - border-width: 0; cursor: pointer; transition: background-color parameters.$middle-duration ease, color parameters.$middle-duration ease; + border: 2px solid rgba(0, 0, 0, 0); } @mixin flex-container($direction: column, $align-items: center, $justify-content: center) { diff --git a/src/scripts/components/auth-menu/views/auth.ts b/src/scripts/components/auth-menu/views/auth.ts index 1872e95..80fcbf1 100644 --- a/src/scripts/components/auth-menu/views/auth.ts +++ b/src/scripts/components/auth-menu/views/auth.ts @@ -17,7 +17,8 @@ export default class AuthView implements AuthViewInterface { const rootElement = document.getElementById(rootId); if (!rootElement) { - return console.error(`Элемент ID = ${rootId} не найден`); + //// console.error(`Элемент ID = ${rootId} не найден`); + return new Error(`Элемент ID = ${rootId} не найден`) } rootElement.innerHTML = ''; diff --git a/src/scripts/components/auth-menu/views/configs.ts b/src/scripts/components/auth-menu/views/configs.ts index d4d3f31..afe57e9 100644 --- a/src/scripts/components/auth-menu/views/configs.ts +++ b/src/scripts/components/auth-menu/views/configs.ts @@ -60,7 +60,7 @@ export const menuSignUp = { ], submitText: 'Регистрация', link: { - href: 'login', + href: '/login', text: 'Есть аккаунт?', label: 'Войти', }, @@ -114,7 +114,7 @@ export const menuSignIn = { ], submitText: 'Войти', link: { - href: 'signup', + href: '/signup', text: 'Нет аккаунта?', label: 'Зарегистрироваться', }, diff --git a/src/scripts/components/auth-menu/views/form.scss b/src/scripts/components/auth-menu/views/form.scss index 82e0802..3c79ecd 100644 --- a/src/scripts/components/auth-menu/views/form.scss +++ b/src/scripts/components/auth-menu/views/form.scss @@ -53,12 +53,6 @@ font-weight: bold; margin-bottom: parameters.$midle-button-margin-bottom; - &:hover { - background-color: parameters.$darker-background-color; - border: 2px solid parameters.$darker-background-color; - transform: scale(parameters.$big-scale); - } - &:active { background-color: parameters.$darkest-background-color; border: 2px solid parameters.$darkest-background-color; diff --git a/src/scripts/components/card/presenter/card.ts b/src/scripts/components/card/presenter/card.ts index a37d232..0a28fb7 100644 --- a/src/scripts/components/card/presenter/card.ts +++ b/src/scripts/components/card/presenter/card.ts @@ -30,9 +30,9 @@ export class CardPresenter { this.view.render({ products: cardsData }); }) - .catch((err) => { - console.error(err); - }); + // .catch((err) => { + // // console.error(err); + // }); }; private attachCardClickHandlers() { diff --git a/src/scripts/components/card/view/card.hbs b/src/scripts/components/card/view/card.hbs index b5cb44a..d158e9d 100644 --- a/src/scripts/components/card/view/card.hbs +++ b/src/scripts/components/card/view/card.hbs @@ -9,7 +9,7 @@
{{page_title}}
-
+ {{#if sorting}}
{{/if}} @@ -27,7 +27,7 @@
-
+
{{costFormat price}}₽ {{#if original_price}} @@ -36,6 +36,7 @@ {{#if discount}} -{{discount}}% {{/if}} +
-
{{title}}
@@ -64,4 +64,8 @@
{{/each}} + + {{#if more}} + + {{/if}} \ No newline at end of file diff --git a/src/scripts/components/card/view/card.ts b/src/scripts/components/card/view/card.ts index 597f128..3328105 100644 --- a/src/scripts/components/card/view/card.ts +++ b/src/scripts/components/card/view/card.ts @@ -1,9 +1,10 @@ import card from './card.hbs?raw'; import { rootId } from '@/services/app/config'; import Handlebars from 'handlebars'; +import {b} from "vite/dist/node/types.d-aGj9QkWt"; export interface CardViewInterface { - render(data: { products: any[], page_title?: string }, title?: string): void; + render(data: { products: any[], page_title?: string }, title?: string, newRootId?: string, sorting?: boolean, flag?: boolean, url?: string): void; } export class CardView implements CardViewInterface { @@ -13,15 +14,17 @@ export class CardView implements CardViewInterface { this.compiled = Handlebars.compile(card); } - render = (data: { products: any[], page_title?: string }, title?: string): void => { - const rootElement = document.getElementById(rootId); + render = (data: { products: any[], page_title?: string }, title?: string, newRootId: string = rootId, sorting: boolean = true, flag: boolean = false, url: string = ''): void => { + + data.sorting = sorting; + + const rootElement = document.getElementById(newRootId); + if (!rootElement) { - console.log(`ошибка rootElement ${rootElement} -- rootId ${rootId}`); + // console.log(`ошибка rootElement ${rootElement} -- rootId ${rootId}`); return; } - console.log(data.page_title, title); - rootElement.innerHTML = ''; const templateElement = document.createElement('div'); @@ -34,5 +37,18 @@ export class CardView implements CardViewInterface { templateElement.innerHTML = this.compiled(data); rootElement.appendChild(templateElement); + + if (flag) { + this.initButton(flag, url); + } + }; + + initButton = (flag: boolean, url: string) => { + const button = document.getElementById('show-more') + + if (flag && button) { + button.setAttribute('href', url); + button.setAttribute('router', 'changed-active'); + } }; } diff --git a/src/scripts/components/card/view/catalog.scss b/src/scripts/components/card/view/catalog.scss index 594f773..b262f17 100644 --- a/src/scripts/components/card/view/catalog.scss +++ b/src/scripts/components/card/view/catalog.scss @@ -33,7 +33,6 @@ padding: parameters.$padding; display: flex; flex-direction: column; - justify-content: space-between; align-items: center; text-align: center; position: relative; @@ -86,9 +85,22 @@ } } - &__card-info { - width: 100%; + .product-page__rating { + margin-bottom: 0; + display: flex; + gap: 1px; + } + + &__wrap { display: flex; + margin-bottom: 10px; + gap: 15px; + width: 100%; + } + + &__card-info { + width: 70%; + display: inline-block; flex-direction: column; align-items: center; @@ -116,7 +128,6 @@ font-size: 16px; color: parameters.$color-discount; margin-left: 8px; - margin-right: 40px; } } @@ -158,6 +169,12 @@ &:hover { transform: none; } + + &__show-more { + @include mixins.checkout-btn(); + margin: 10px; + width: calc(100% - 20px); + } } /* Центрирование и стили для сообщения ожидания */ @@ -182,6 +199,12 @@ } } +.reviews-header-scroll { + display: inline-flex; + align-items: center; + width: 68px; +} + @keyframes spin { to { transform: rotate(360deg); @@ -191,7 +214,7 @@ /* Медиазапросы для адаптивности */ @media (max-width: parameters.$phone-smallest-header) { .catalog { - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));; width: 100%; padding: parameters.$padding 10px; @@ -208,6 +231,10 @@ width: 100%; text-align: center; } + + &__wrap { + display: block; + } } } @@ -223,7 +250,7 @@ } &__card-image { - height: 300px; + height: 200px; } &__buy-button { @@ -231,4 +258,4 @@ text-align: center; } } -} \ No newline at end of file +} diff --git a/src/scripts/components/cart/api/cart-api.ts b/src/scripts/components/cart/api/cart-api.ts index 33b29fb..8f431c6 100644 --- a/src/scripts/components/cart/api/cart-api.ts +++ b/src/scripts/components/cart/api/cart-api.ts @@ -31,7 +31,7 @@ export class CartApiInterface { static async updateProductQuantity(productId: string, count: number): Promise { return csrf.patch(`${backurl}${CART_URLS.updateProductQuantity.route}${productId}`, { count }) .then(res => { - if (res.status !== 204) { + if (res.status !== 200) { throw new Error(`Ошибка при обновлении количества: ${res.status} - ${res.body.error_message}`); } }) @@ -54,13 +54,13 @@ export class CartApiInterface { csrf.refreshToken(); } - console.error(`ошибка при выборе: ${res.status} - ${res.body}`); + //// console.error(`ошибка при выборе: ${res.status} - ${res.body}`); throw new Error(`${res.body}`); }) - .catch(err => { - console.error(err); - }) + /*.catch(err => { + // console.error(err); + })*/ } /** @@ -79,13 +79,13 @@ export class CartApiInterface { csrf.refreshToken(); } - console.error(`ошибка при выборе: ${res.status} - ${res.body}`); + //// console.error(`ошибка при выборе: ${res.status} - ${res.body}`); throw new Error(`${res.body}`); }) - .catch(err => { - console.error(err); - }) + /*.catch(err => { + // console.error(err); + })*/ } /** diff --git a/src/scripts/components/cart/elements/data-sampling/presenter/data-sampling.ts b/src/scripts/components/cart/elements/data-sampling/presenter/data-sampling.ts index cbd1a2c..a03643f 100644 --- a/src/scripts/components/cart/elements/data-sampling/presenter/data-sampling.ts +++ b/src/scripts/components/cart/elements/data-sampling/presenter/data-sampling.ts @@ -72,7 +72,7 @@ export class DataSamplingPresenter { this.cartView.initializeCheckboxes(this.cartData.products); this.updateSelectedCount();}); } catch (error) { - console.error("Ошибка при изменении состояния 'Выбрать всё':", error); + //// console.error("Ошибка при изменении состояния 'Выбрать всё':", error); } } @@ -126,7 +126,7 @@ export class DataSamplingPresenter { } }) } catch (error) { - console.error("Ошибка при изменении состояния 'Удалить выбранное':", error); + //// console.error("Ошибка при изменении состояния 'Удалить выбранное':", error); } } @@ -151,7 +151,7 @@ export class DataSamplingPresenter { this.cartView.updateSelectAllCheckbox(allChecked, isIndeterminate); } catch (error) { - console.error("Ошибка при изменении состояния 'Выбрать всё':", error); + //// console.error("Ошибка при изменении состояния 'Выбрать всё':", error); } } } diff --git a/src/scripts/components/cart/elements/left-cards/presenter/left-cards.ts b/src/scripts/components/cart/elements/left-cards/presenter/left-cards.ts index 2526482..f1f9e50 100644 --- a/src/scripts/components/cart/elements/left-cards/presenter/left-cards.ts +++ b/src/scripts/components/cart/elements/left-cards/presenter/left-cards.ts @@ -67,6 +67,10 @@ export class LeftCardsPresenter { this.view.updateSelectedCount(selectedCount); this.rightCartPresenter.calculateCartTotals(this.cartData); + if (this.cartData.products.length === 0) { + LeftCardsView.displayEmptyCartMessage(); + } + // Обновляем состояние чекбокса "Выбрать все" this.updateSelectAllCheckbox(); } @@ -125,7 +129,7 @@ export class LeftCardsPresenter { await this.rightCartPresenter.calculateCartTotals(this.cartData); }) } catch (error) { - console.error('Ошибка при обновлении количества товара:', error); + //// console.error('Ошибка при обновлении количества товара:', error); } } } @@ -149,7 +153,7 @@ export class LeftCardsPresenter { this.checkIfCartIsEmpty(); }) } catch (error) { - console.error('Ошибка при удалении товара из корзины:', error); + //// console.error('Ошибка при удалении товара из корзины:', error); } } @@ -170,7 +174,7 @@ export class LeftCardsPresenter { } }) } catch (error) { - console.error('Ошибка при выборе товара в корзине:', error); + //// console.error('Ошибка при выборе товара в корзине:', error); } } diff --git a/src/scripts/components/cart/elements/left-cards/view/left-cards.hbs b/src/scripts/components/cart/elements/left-cards/view/left-cards.hbs index 59b6cb3..8565bfa 100644 --- a/src/scripts/components/cart/elements/left-cards/view/left-cards.hbs +++ b/src/scripts/components/cart/elements/left-cards/view/left-cards.hbs @@ -40,13 +40,10 @@ {{/each}} -{{else}} -
- - - - -

Ваша корзина пуста

-

Добавьте товары, чтобы увидеть их здесь!

-
{{/if}} + +
+ Crying Man +

Ваша корзина пуста

+

Добавьте товары, чтобы увидеть их здесь!

+
diff --git a/src/scripts/components/cart/elements/left-cards/view/left-cards.scss b/src/scripts/components/cart/elements/left-cards/view/left-cards.scss index 09930a8..a4501b7 100644 --- a/src/scripts/components/cart/elements/left-cards/view/left-cards.scss +++ b/src/scripts/components/cart/elements/left-cards/view/left-cards.scss @@ -16,9 +16,8 @@ } &__image { - width: parameters.$card-image-width; padding: parameters.$micro-padding; - min-width: parameters.$min-card-image-width; + width: parameters.$min-card-image-width; img { width: parameters.$width-max; @@ -146,38 +145,7 @@ @include basic-mixins.checkbox; - &__placeholder { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: 30px; - text-align: center; - font-size: 20px; - font-weight: bold; - color: parameters.$default-text-color; - background-color: #f9f9f9; - border-radius: 12px; - box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); - animation: fadeIn 1s ease-in-out; - } - - &__icon { - margin-bottom: 15px; - opacity: 0.7; - transition: transform 0.3s ease; - } - - &__placeholder-text, - &__placeholder-suggestion { - font-size: 18px; - color: #555; - } - - &__placeholder:hover .cart-item__icon { - transform: scale(1.1); - opacity: 1; - } + @include basic-mixins.placeholder; &__left { display: flex; diff --git a/src/scripts/components/cart/elements/left-cards/view/left-cards.ts b/src/scripts/components/cart/elements/left-cards/view/left-cards.ts index 5bedef1..00b8a6a 100644 --- a/src/scripts/components/cart/elements/left-cards/view/left-cards.ts +++ b/src/scripts/components/cart/elements/left-cards/view/left-cards.ts @@ -195,16 +195,10 @@ export class LeftCardsView { * Отображает сообщение о том, что корзина пуста. */ public static displayEmptyCartMessage(): void { - const cartContainer = document.querySelector('.cart-items'); - if (cartContainer) { - cartContainer.innerHTML = '
\n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - '

Ваша корзина пуста

\n' + - '

Добавьте товары, чтобы увидеть их здесь!

\n' + - '
'; + const placeholder = document.getElementById('empty-cart-placeholder'); + + if (placeholder) { + placeholder.style.display = "flex"; } } } diff --git a/src/scripts/components/cart/elements/right-element-of-cart/presenter/calculate-cart-totals.ts b/src/scripts/components/cart/elements/right-element-of-cart/presenter/calculate-cart-totals.ts index 0577606..219fdc6 100644 --- a/src/scripts/components/cart/elements/right-element-of-cart/presenter/calculate-cart-totals.ts +++ b/src/scripts/components/cart/elements/right-element-of-cart/presenter/calculate-cart-totals.ts @@ -42,7 +42,7 @@ export class RightCartPresenter { * @returns {Promise} Промис, разрешающийся после обновления данных корзины. */ async calculateCartTotals(cartData : CartData) { - console.log(cartData); + // console.log(cartData); this.cartData = cartData; const selectedItems = this.cartData.products.filter((product: CartProduct) => product.isSelected); diff --git a/src/scripts/components/cart/elements/right-element-of-cart/view/calculate-cart-totals.ts b/src/scripts/components/cart/elements/right-element-of-cart/view/calculate-cart-totals.ts index d602b1e..9d584c1 100644 --- a/src/scripts/components/cart/elements/right-element-of-cart/view/calculate-cart-totals.ts +++ b/src/scripts/components/cart/elements/right-element-of-cart/view/calculate-cart-totals.ts @@ -72,7 +72,7 @@ export class RightCartView { ) { if (this.totalItemsElement && this.benefitPriceElement && this.finalPriceElement) { this.totalItemsElement.innerHTML = ` - Всего: ${totalItems} ${Helper.pluralize(totalItems, 'товар', 'товара', 'товаров')}, ${Math.round(totalWeight * 10) / 10}кг + Всего: ${totalItems} ${Helper.pluralize(totalItems, 'товар', 'товара', 'товаров')} ${totalPrice} ${currency ? Helper.isNotUndefined(currency) : '₽' } `; @@ -121,7 +121,7 @@ export class RightCartView { // Обработчик клика для вывода в консоль выбранных товаров this.checkoutButton.addEventListener('click', () => { - console.log('Selected products:', selectedItems); + // console.log('Selected products:', selectedItems); }); } } diff --git a/src/scripts/components/cart/elements/right-element-of-cart/view/right-element-of-cart.hbs b/src/scripts/components/cart/elements/right-element-of-cart/view/right-element-of-cart.hbs index fde7428..3857cd9 100644 --- a/src/scripts/components/cart/elements/right-element-of-cart/view/right-element-of-cart.hbs +++ b/src/scripts/components/cart/elements/right-element-of-cart/view/right-element-of-cart.hbs @@ -3,7 +3,7 @@

- Всего: {{totalItems}} {{pluralize totalItems 'товар' 'товара' 'товаров'}}, {{totalWeight}}кг + Всего: {{totalItems}} {{pluralize totalItems 'товар' 'товара' 'товаров'}} {{totalPrice}}{{#if (isNotUndefined currency)}}{{currency}}{{else}}₽{{/if}}

diff --git a/src/scripts/components/cart/presenter/cart.ts b/src/scripts/components/cart/presenter/cart.ts index 30857af..aaa5bea 100644 --- a/src/scripts/components/cart/presenter/cart.ts +++ b/src/scripts/components/cart/presenter/cart.ts @@ -73,15 +73,19 @@ export class CartPresenter { */ async initializeCart() { csrf.fetchToken().then(() =>{ - // Инициализируем чекбоксы на основе данных корзины - this.cartView.initializeCheckboxes(this.cartData.products); + try { + // Инициализируем чекбоксы на основе данных корзины + this.cartView.initializeCheckboxes(this.cartData.products); - // Инициализируем работу с выборкой данных. - this.dataSamplingPresenter.initializeDataSampling(); + // Инициализируем работу с выборкой данных. + this.dataSamplingPresenter.initializeDataSampling(); - // Выполняем дополнительные настройки. - this.leftCardsPresenter.updateSelectedCount(); - this.rightCartPresenter.calculateCartTotals(this.cartData); + // Выполняем дополнительные настройки. + this.leftCardsPresenter.updateSelectedCount(); + this.rightCartPresenter.calculateCartTotals(this.cartData); + } catch (err) { + + } }); } } diff --git a/src/scripts/components/cart/view/cart-builder.ts b/src/scripts/components/cart/view/cart-builder.ts index 03f42f1..f537395 100644 --- a/src/scripts/components/cart/view/cart-builder.ts +++ b/src/scripts/components/cart/view/cart-builder.ts @@ -93,7 +93,7 @@ export class CartBuilder { await this.renderCart(); this.initializeCartPresenter(); } catch (error) { - console.error(error); + // console.error(error); } } diff --git a/src/scripts/components/category/api/category.ts b/src/scripts/components/category/api/category.ts index 3511456..9b99a10 100644 --- a/src/scripts/components/category/api/category.ts +++ b/src/scripts/components/category/api/category.ts @@ -40,7 +40,7 @@ export class CategoryApi implements CategoryApiInterface { return fetch(`${backurl}/categories`) .then((response) => response.json()) .then((data) => { - console.log(data); + // console.log(data); return data; }) .then((data: ApiResponse) => { @@ -65,7 +65,7 @@ export class CategoryApi implements CategoryApiInterface { return data as ProductError; }) .catch(error =>{ - console.log(error); + // console.log(error); return error; }) diff --git a/src/scripts/components/category/presenter/category.ts b/src/scripts/components/category/presenter/category.ts index aa7670a..51d3a42 100644 --- a/src/scripts/components/category/presenter/category.ts +++ b/src/scripts/components/category/presenter/category.ts @@ -31,7 +31,7 @@ export class CategoryPresenter { this.api.getCategories() .then((data) => { if (typeof data === 'string') { - console.error(data); + // console.error(data); } else { data.forEach((card) => { card.picture = `${backurl}/${card.picture}`; @@ -93,7 +93,7 @@ export class CategoryPresenter { if (order) urlParams.append('order', order); const categoryUrl = `${backurl}/category/${categoryLink}${urlParams.toString() ? `?${urlParams.toString()}` : ''}`; - console.log(categoryUrl); + // console.log(categoryUrl); this.api.getCategoryProducts(categoryUrl) .then((products) => { if (Array.isArray(products)) { @@ -105,7 +105,7 @@ export class CategoryPresenter { this.categoryView.renderCategoryProducts({ products }, categoryLink); this.updateBreadcrumbs(`Категория: ${categoryLink}`, `/category/${categoryLink}${urlParams.toString() ? `?${urlParams.toString()}` : ''}`); } else { - console.error('No products found in category'); + // console.error('No products found in category'); } }) .then(() => { @@ -128,11 +128,11 @@ export class CategoryPresenter { this.dropdownPresenter.initView(); } else { - console.error('Dropdown container not found'); + // console.error('Dropdown container not found'); } }) .catch((e) => { - console.error('Error fetching category products:', e); + // console.error('Error fetching category products:', e); }); } diff --git a/src/scripts/components/category/view/category.ts b/src/scripts/components/category/view/category.ts index 95a1e95..808d512 100644 --- a/src/scripts/components/category/view/category.ts +++ b/src/scripts/components/category/view/category.ts @@ -32,16 +32,40 @@ export class CategoryView implements CategoryViewInterface { root.innerHTML = htmlContent; } + private transform (link: string): string { + let name = '' + + switch (link) { + case 'perfumery': + name = 'Парфюмерия' + break; + case 'appliances': + name = 'Бытовая техника' + break; + case 'clothes': + name = 'Одежда' + break; + case 'electronics': + name = 'Электроника' + break; + default: + name = 'Другое' + break; + } + + return name; + } + renderCategoryProducts = (products: { products: any[] , page_title?: string }, link: string): void => { - products.page_title = 'Категория: '; + products.page_title = `Категория: `; - this.cardView.render(products, link); + this.cardView.render(products, this.transform(link)); } renderBreadcrumbs = (breadcrumbs: { title: string, link: string }[]) => { // const breadcrumbContainer = document.getElementById('breadcrumbs'); // if (!breadcrumbContainer) { - // console.error('Не найден контейнер для хлебных крошек'); + // // console.error('Не найден контейнер для хлебных крошек'); // return; // } // diff --git a/src/scripts/components/custom-messages/error/error.hbs b/src/scripts/components/custom-messages/error/error.hbs index 1d6ca7d..8c90ac8 100644 --- a/src/scripts/components/custom-messages/error/error.hbs +++ b/src/scripts/components/custom-messages/error/error.hbs @@ -1,5 +1,5 @@

Ошибка {{name}}

{{description}}

-

Нажмите, сюда, чтобы вернуться на главную!

+

Нажмите сюда, чтобы вернуться на главную!

\ No newline at end of file diff --git a/src/scripts/components/custom-messages/error/error.ts b/src/scripts/components/custom-messages/error/error.ts index c085b84..ea5a6de 100644 --- a/src/scripts/components/custom-messages/error/error.ts +++ b/src/scripts/components/custom-messages/error/error.ts @@ -15,7 +15,7 @@ const returnPage = '/catalog'; * @returns {Promise} Промис, который разрешается после успешного отображения страницы ошибки. */ export function errorPage(name: string): void { - let config = { + const config = { name: name, description: errorsDescriptions[name], return: returnPage, @@ -25,7 +25,7 @@ export function errorPage(name: string): void { const rootElement = document.getElementById(rootId) as HTMLElement; if (!rootElement) { - console.error(`Element ID = ${rootId} not found`); + // console.error(`Element ID = ${rootId} not found`); } rootElement.innerHTML = ''; diff --git a/src/scripts/components/custom-messages/error/errors.ts b/src/scripts/components/custom-messages/error/errors.ts index 37926f3..8a675d5 100644 --- a/src/scripts/components/custom-messages/error/errors.ts +++ b/src/scripts/components/custom-messages/error/errors.ts @@ -5,5 +5,5 @@ * @property {string} '404' - Описание ошибки 404 (Не найдено), отображаемое при попытке обращения к несуществующему адресу. */ export const errorsDescriptions: { [key: string]: string } = { - 404: 'Вы обращаетесь на несуществующему адресу.', + 404: 'Вы обращаетесь к несуществующему адресу.', }; diff --git a/src/scripts/components/custom-messages/soon/soon.hbs b/src/scripts/components/custom-messages/soon/soon.hbs index 6ba1d4a..c16d771 100644 --- a/src/scripts/components/custom-messages/soon/soon.hbs +++ b/src/scripts/components/custom-messages/soon/soon.hbs @@ -1,5 +1,5 @@

Скоро

Будет добавлено позже.

- +
\ No newline at end of file diff --git a/src/scripts/components/dropdown-btn/api/dropdown.ts b/src/scripts/components/dropdown-btn/api/dropdown.ts index b315f96..5e292eb 100644 --- a/src/scripts/components/dropdown-btn/api/dropdown.ts +++ b/src/scripts/components/dropdown-btn/api/dropdown.ts @@ -16,14 +16,23 @@ export class DropdownAPI { } - static sortProducts = (endpoint: string, sort: string, order: string): Promise => { + static sortProducts = async (endpoint: string, sort: string, order: string): Promise | undefined => { const params = new URLSearchParams({ sort, order, }); - console.log(123, `${endpoint}&${params.toString()}`); - - return get(`${endpoint}&${params.toString()}`) + try { + const response = await get(`${endpoint}&${params.toString()}`); + if (response === undefined) { + console.log("Response is undefined", response); + return undefined; // Либо можно обработать это как-то по-другому + } else { + return response; + } + } catch { + return undefined; + } + // console.log(123, `${endpoint}&${params.toString()}`); }; } diff --git a/src/scripts/components/dropdown-btn/presenter/dropdown.ts b/src/scripts/components/dropdown-btn/presenter/dropdown.ts index 3f0735f..04fa193 100644 --- a/src/scripts/components/dropdown-btn/presenter/dropdown.ts +++ b/src/scripts/components/dropdown-btn/presenter/dropdown.ts @@ -22,10 +22,10 @@ export class DropdownPresenter { initView(): void { const container = document.getElementById(this.config.containerId); - console.log('init view'); + // console.log('init view'); if (!container) { - console.error(`Контейнер с ID ${this.config.containerId} не найден`); + // console.error(`Контейнер с ID ${this.config.containerId} не найден`); return; } diff --git a/src/scripts/components/modal/presenters/base-modal.ts b/src/scripts/components/modal/presenters/base-modal.ts index 6687c15..8f7722a 100644 --- a/src/scripts/components/modal/presenters/base-modal.ts +++ b/src/scripts/components/modal/presenters/base-modal.ts @@ -29,7 +29,7 @@ export abstract class BaseModal { // // const closeButton = this.modalElement.querySelector(this.config.btnClose); // if (!closeButton) { - // console.error(closeButton, this.config.btnClose); + // // console.error(closeButton, this.config.btnClose); // return; // } // @@ -48,19 +48,26 @@ export abstract class BaseModal { }, 300); } } - private handleOutsideClick(event: MouseEvent) { + const modalContent = this.modalElement?.querySelector('.modal__container'); + const suggestionsList = document.querySelector('.suggestions'); + const suggestionItem = event.target && (event.target as HTMLElement).closest('.suggestions__item'); + if (!this.modalElement) { - console.error(this.modalElement, ' not found'); + // console.error(this.modalElement, ' not found'); return; } - const modalContent = this.modalElement.querySelector('.modal__container'); - if (modalContent && !modalContent.contains(event.target as Node)) { + const isClickInsideModal = modalContent && modalContent.contains(event.target as Node); + const isClickInsideSuggestions = suggestionsList && (suggestionsList.contains(event.target as Node) || suggestionItem); + + // Close the modal only if the click is outside both modal content and suggestions + if (!isClickInsideModal && !isClickInsideSuggestions) { this.close(); } } + protected abstract renderContent(): void; protected handleFormSubmission( diff --git a/src/scripts/components/modal/presenters/change-password.ts b/src/scripts/components/modal/presenters/change-password.ts new file mode 100644 index 0000000..6ec414b --- /dev/null +++ b/src/scripts/components/modal/presenters/change-password.ts @@ -0,0 +1,134 @@ +import { BaseModal } from './base-modal'; +import { changePasswordConfig, ModalControllerParams } from '../views/types'; +import { ModalRenderer } from '../views/modal-render'; +import { backurl } from '../../../../services/app/config'; +import { csrf } from '../../../../services/api/CSRFService'; + +export class ChangePasswordModal extends BaseModal { + private readonly onSubmitCallback: () => void; + + constructor( + config: ModalControllerParams, + onSubmit: () => void, + ) { + super(config, changePasswordConfig, {}); + this.onSubmitCallback = onSubmit; + } + + protected renderContent(): void { + changePasswordConfig.fields.forEach((field) => { + field.value = ''; + }); + + this.modalElement = ModalRenderer.render(this.config.rootId, changePasswordConfig); + this.setupListeners(); + } + + protected setupListeners() { + if (!this.modalElement) return; + const form = this.modalElement.querySelector(`#${changePasswordConfig.formId}`) as HTMLFormElement; + if (!form) return; + + form.addEventListener('submit', async (event) => { + event.preventDefault(); + + if (!this.validateForm()) { + return; + } + + const formData = new FormData(form); + const updatedPassword: Record = {}; + + formData.forEach((value, key) => { + updatedPassword[key] = value as string; + }); + + // Отправка данных на сервер для смены пароля + return csrf.put(`${backurl}/change_password`, updatedPassword) + .then(res => { + switch (res.status) { + case 200: + this.onSubmitCallback(); + this.close(); + return; + case 403: + csrf.refreshToken(); + throw new Error('протух csrf токен, попробуйте еще раз'); + default: + throw new Error(`${res.status} - ${res.body.error_message}`); + } + }) + .catch((err) => { + // console.error('Ошибка смены пароля:', err); + }); + }); + + if (!this.modalElement) { + return; + } + + const closeButton = this.modalElement.querySelector('.btn__close'); + closeButton?.addEventListener('click', this.close.bind(this)); + } + + private validateForm(): boolean { + let isValid = true; + + changePasswordConfig.fields.forEach((field) => { + const inputElement = this.modalElement?.querySelector(`[name="${field.name}"]`) as HTMLInputElement; + + const regex = /^[a-zA-Z0-9!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]+$/u; + this.removeInputError(inputElement); + isValid = true; + + if (inputElement && !inputElement.value.trim()) { + this.addInputError(inputElement, `Поле "${field.label}" не должно быть пустым`); + isValid = false; + } else if (field.name === 'repeatPassword' && inputElement && inputElement.value !== this.modalElement?.querySelector('[name="new-password"]')?.value) { + this.addInputError(inputElement, 'Пароли не совпадают'); + isValid = false; + } else if (!regex.test(inputElement.value)) { + this.addInputError(inputElement, 'Пароль должен содержать только допустимые символы'); + isValid = false; + } + }); + + return isValid; + } + + private addInputError(element: HTMLElement, message: string) { + const errorId = element.getAttribute('data-error-id') || `${element.name}Error`; + let errorElement = document.getElementById(errorId); + + if (!errorElement) { + errorElement = document.createElement('div'); + errorElement.id = errorId; + errorElement.className = 'form__error'; + element.parentElement?.appendChild(errorElement); + } + + errorElement.textContent = message; + errorElement.style.display = 'block'; + element.classList.add('form__input_invalid'); + element.style.backgroundColor = '#ffe6e6'; + element.style.borderColor = 'red'; + } + + private removeInputError(element: HTMLElement) { + const errorId = element.getAttribute('data-error-id') || `${element.name}Error`; + const errorElement = document.getElementById(errorId); + + if (errorElement) { + errorElement.style.display = 'none'; + errorElement.textContent = ''; + element.classList.remove('form__input_invalid'); + element.style.borderColor = ''; + element.style.backgroundColor = ''; + } + } + + public open() { + this.renderContent(); + super.open(); + } +} diff --git a/src/scripts/components/modal/presenters/modal-address.ts b/src/scripts/components/modal/presenters/modal-address.ts index d0bf1de..9a3b9d9 100644 --- a/src/scripts/components/modal/presenters/modal-address.ts +++ b/src/scripts/components/modal/presenters/modal-address.ts @@ -3,11 +3,17 @@ import { editAddressConfig, ModalControllerParams } from '../views/types'; import { ModalRenderer } from '../views/modal-render'; import { backurl } from '../../../../services/app/config'; import { csrf } from '../../../../services/api/CSRFService'; +import { debounce } from '../../../utils/debounce'; +import { API_KEY } from './key-yandex'; export class AddressModal extends BaseModal { private readonly onSubmitCallback: (updatedAddress: Record) => void; - constructor(config: ModalControllerParams, userAddress: Record, onSubmit: (updatedAddress: Record) => void) { + constructor( + config: ModalControllerParams, + userAddress: Record, + onSubmit: (updatedAddress: Record) => void, + ) { super(config, editAddressConfig, userAddress); this.onSubmitCallback = onSubmit; } @@ -26,46 +32,101 @@ export class AddressModal extends BaseModal { const form = this.modalElement.querySelector(`#${editAddressConfig.formId}`) as HTMLFormElement; if (!form) return; - form.addEventListener('submit', async (event) => { - event.preventDefault(); + const addressInput = this.modalElement.querySelector(`[name="address"]`) as HTMLInputElement; + const suggestionsList = this.modalElement.querySelector('#suggestions-list') as HTMLElement; + + if (addressInput && suggestionsList) { + addressInput.addEventListener( + 'input', + debounce(async () => { + const query = addressInput.value.trim(); + if (query.length < 3) { + suggestionsList.innerHTML = ''; // Очистка списка предложений + return; + } - if (!this.validateForm()) { - console.log("Validation failed: Some required fields are empty."); - return; - } + const suggestions = await this.fetchAddressSuggestions(query); - const formData = new FormData(form); - const updatedAddress: Record = {}; + // Обновляем UI для показа подсказок + this.updateSuggestionsList(suggestionsList, suggestions, addressInput); + }, 300), + ); - formData.forEach((value, key) => { - updatedAddress[key] = value as string; - }); + form.addEventListener('submit', async (event) => { + event.preventDefault(); + + if (!this.validateForm()) { + // console.log('Validation failed: Some required fields are empty.'); + return; + } + + const formData = new FormData(form); + const updatedAddress: Record = {}; + + formData.forEach((value, key) => { + updatedAddress[key] = value as string; + }); return csrf.put(`${backurl}/address`, updatedAddress) .then(res => { - switch (res.status){ + switch (res.status) { case 200: this.onSubmitCallback(updatedAddress); this.close(); return; case 403: csrf.refreshToken(); - throw new Error('протух csrf токен, попробуйте еще раз') + throw new Error('протух csrf токен, попробуйте еще раз'); default: throw new Error(`${res.status} - ${res.body.error_message}`); } }) .catch((err) => { - console.error('Error updating address:', err); - }) + // console.error('Error updating address:', err); + }); }); - if (!this.modalElement) { - return; + if (!this.modalElement) { + return; + } + + const closeButton = this.modalElement.querySelector('.btn__close'); + closeButton?.addEventListener('click', this.close.bind(this)); } + } + + private updateSuggestionsList(suggestionsList: HTMLElement, suggestions: any[], addressInput: HTMLInputElement) { + suggestionsList.innerHTML = ''; // Очищаем текущие подсказки - const closeButton = this.modalElement.querySelector('.btn__close'); - closeButton?.addEventListener('click', this.close.bind(this)); + suggestions.forEach((suggestion) => { + const option = document.createElement('div'); + option.className = 'suggestions__item'; + option.textContent = suggestion.address.formatted_address; + + option.addEventListener('click', () => { + addressInput.value = suggestion.address.formatted_address; + suggestionsList.innerHTML = ''; + }); + + suggestionsList.appendChild(option); + }); + } + + private async fetchAddressSuggestions(query: string): Promise { + const url = `https://suggest-maps.yandex.ru/v1/suggest?apikey=${API_KEY}&text=${encodeURIComponent(query)}&print_address=1&attrs=uri`; + + try { + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Ошибка API Яндекса: ${response.status}`); + } + + const data = await response.json(); + return data.results || []; + } catch (error) { + console.error('Ошибка получения подсказок адреса:', error); + return []; + } } private validateForm(): boolean { @@ -74,11 +135,16 @@ export class AddressModal extends BaseModal { editAddressConfig.fields.forEach((field) => { const inputElement = this.modalElement?.querySelector(`[name="${field.name}"]`) as HTMLInputElement; - if (inputElement && field.name !== 'flat' && !inputElement.value.trim()) { + const regex = /^[a-zA-Zа-яА-Я0-9№\s.,\/-]+$/u; + this.removeInputError(inputElement); + isValid = true; + + if (inputElement && !inputElement.value.trim()) { this.addInputError(inputElement, `Поле "${field.label}" не должно быть пустым`); isValid = false; - } else { - this.removeInputError(inputElement); + } else if (!regex.test(inputElement.value)) { + this.addInputError(inputElement, 'Поле должно содержать буквы цифры, точки, запятые, слэши и дефисы') + isValid = false; } }); @@ -120,4 +186,4 @@ export class AddressModal extends BaseModal { this.renderContent(); super.open(); } -} \ No newline at end of file +} diff --git a/src/scripts/components/modal/presenters/modal-factory.ts b/src/scripts/components/modal/presenters/modal-factory.ts index 2e23605..7ea25e8 100644 --- a/src/scripts/components/modal/presenters/modal-factory.ts +++ b/src/scripts/components/modal/presenters/modal-factory.ts @@ -26,7 +26,7 @@ export class ModalFactory { const ModalClass = modalRegistry[modalType]; if (!ModalClass) { - console.warn(`Unknown modal type: ${modalType}`); + // console.warn(`Unknown modal type: ${modalType}`); return null; } diff --git a/src/scripts/components/modal/presenters/modal-personal.ts b/src/scripts/components/modal/presenters/modal-personal.ts index ee2560c..96162d7 100644 --- a/src/scripts/components/modal/presenters/modal-personal.ts +++ b/src/scripts/components/modal/presenters/modal-personal.ts @@ -40,9 +40,9 @@ export class PersonalDataModal extends BaseModal { updatedUser[key] = value as string; }); - this.handleSubmit(updatedUser) - .then(() => console.log("Submit successful")) - .catch((error) => console.error("Submit failed:", error)); + this.handleSubmit(updatedUser).finally() + // .then(() => console.log("Submit successful")) + // .catch((error) => console.error("Submit failed:", error)); }); const inputs = form.querySelectorAll('input, select') as HTMLInputElement[]; @@ -91,7 +91,7 @@ export class PersonalDataModal extends BaseModal { } }) .catch(err => { - console.error('Error updating profile:', err); + // console.error('Error updating profile:', err); this.displayBackError(err || 'ошибка, попробуйте еще раз'); }); } @@ -104,7 +104,7 @@ export class PersonalDataModal extends BaseModal { globalErrorMessage.innerText = message; errorElement.style.display = 'block'; } else { - console.error('Error elements not found in the modal.'); + // console.error('Error elements not found in the modal.'); } } @@ -162,7 +162,7 @@ export class PersonalDataModal extends BaseModal { } private addInputError(element: HTMLElement, errorElement: HTMLElement | null, message: string) { - console.log('Adding input error for:', element.id, 'with message:', message); + // console.log('Adding input error for:', element.id, 'with message:', message); if (errorElement) { errorElement.textContent = message; @@ -174,7 +174,7 @@ export class PersonalDataModal extends BaseModal { } private removeInputError(element: HTMLElement, errorElement: HTMLElement | null) { - console.log('Removing input error for:', element.id); + // console.log('Removing input error for:', element.id); if (errorElement) { errorElement.textContent = ''; diff --git a/src/scripts/components/modal/views/modal-render.ts b/src/scripts/components/modal/views/modal-render.ts index 594ede8..379e77c 100644 --- a/src/scripts/components/modal/views/modal-render.ts +++ b/src/scripts/components/modal/views/modal-render.ts @@ -7,7 +7,7 @@ export class ModalRenderer { public static render(rootId: string, data: any): HTMLElement | null { const root = document.getElementById(rootId) as HTMLElement | null; if (!root) { - console.error(`Root element with id ${rootId} not found.`); + // console.error(`Root element with id ${rootId} not found.`); return null; } diff --git a/src/scripts/components/modal/views/modal.hbs b/src/scripts/components/modal/views/modal.hbs index 705376b..b5cae7b 100644 --- a/src/scripts/components/modal/views/modal.hbs +++ b/src/scripts/components/modal/views/modal.hbs @@ -22,7 +22,8 @@ {{/each}} {{else}} - + +
{{/if}}
diff --git a/src/scripts/components/modal/views/modal.scss b/src/scripts/components/modal/views/modal.scss index 02a90ca..e30628c 100644 --- a/src/scripts/components/modal/views/modal.scss +++ b/src/scripts/components/modal/views/modal.scss @@ -1,5 +1,5 @@ @use "../../../../../public/css/parametrs/parameters"; - +@use "src/scripts/components/searcher/view/suggestions.scss"; .modal { position: fixed; top: 0; @@ -12,7 +12,6 @@ &__main { font-family: parameters.$mega-font-family, sans-serif; font-weight: parameters.$litle-font-weight; - display: flex; flex-direction: column; justify-content: center; align-items: center; @@ -28,6 +27,11 @@ cursor: default; padding: parameters.$big-padding; z-index:5000; + + + @media (max-width: 1000px) { + display: flex; + } } &__container { @@ -78,6 +82,7 @@ &:focus { outline: none; } + } &__submit-button { diff --git a/src/scripts/components/modal/views/types.ts b/src/scripts/components/modal/views/types.ts index 0c41d43..112af8c 100644 --- a/src/scripts/components/modal/views/types.ts +++ b/src/scripts/components/modal/views/types.ts @@ -48,43 +48,54 @@ export const editNameGenderEmailConfig = { submitText: 'Сохранить изменения', }; -export const editAddressConfig = { - id: 'edit_address', +export const changePasswordConfig = { + id: 'edit_password', title: 'Редактировать', - formId: 'address-edit-form', + formId: 'user-edit-form', fields: [ { - id: 'user-city', - label: 'Город', - type: 'text', - name: 'city', + id: 'old-pass', + label: 'Старый пароль', + type: 'password', + name: 'old-password', value: '', - error_id: 'city-error', + error_id: 'passwordError', }, { - id: 'user-street', - label: 'Улица', - type: 'text', - name: 'street', + id: 'new-pass', + label: 'Новый пароль', + type: 'newPassword', + name: 'old-password', value: '', - error_id: 'street-error', + error_id: 'newPasswordError', }, { - id: 'user-house', - label: 'Дом', - type: 'text', - name: 'house', + id: 'repeat-pass', + label: 'Повторите', + type: 'password', + name: 'repeatPassword', value: '', - error_id: 'house-error', + error_id: 'repeatPasswordError', }, + ] as ModalField[], + submitText: 'Сохранить изменения', +}; + + + +export const editAddressConfig = { + id: 'edit_address', + title: 'Редактировать', + formId: 'address-edit-form', + fields: [ { - id: 'user-flat', - label: 'Квартира', + id: 'user-address', + label: 'Введите ваш адрес', type: 'text', - name: 'flat', + name: 'address', value: '', - error_id: 'flat-error', - } + error_id: 'city-error', + }, ], submitText: 'Сохранить изменения', }; diff --git a/src/scripts/components/notice/api/csat.ts b/src/scripts/components/notice/api/csat.ts new file mode 100644 index 0000000..baace92 --- /dev/null +++ b/src/scripts/components/notice/api/csat.ts @@ -0,0 +1,16 @@ +import {getWithCred} from "../../../../services/api/without-csrf"; +import {backurl} from "../../../../services/app/config"; + +export interface CsatApiInterface { + getData(): object | null; +} + +export class CsatApi { + static async getData(): object | null { + try { + return await getWithCred(backurl + '/orders/updates'); + } catch { + return null; + } + } +} \ No newline at end of file diff --git a/src/scripts/components/notice/presenters/csat.ts b/src/scripts/components/notice/presenters/csat.ts new file mode 100644 index 0000000..b2928a3 --- /dev/null +++ b/src/scripts/components/notice/presenters/csat.ts @@ -0,0 +1,42 @@ +import Handlebars from 'handlebars'; +import csatTemplate from './csat.hbs'; +import {csatView, csatViewInterface} from "../view/csat"; +import {CsatApi} from "../api/csat"; + +export interface csatPresenterInterface { + render(): void; +} + +export class csatPresenter implements csatPresenterInterface { + private readonly compile: HandlebarsTemplates; + private readonly title: HTMLElement; + private readonly text: HTMLElement; + private readonly view: any; + + constructor(containerId: string) { + this.view = new csatView(containerId); + + this.startPolling(); + } + + private startPolling() { + this.updateData(); + setInterval(() => this.updateData(), 60 * 1000); // Раз в минут + } + + private async updateData() { + try { + const data = await CsatApi.getData(); + + if (data && data.body.orders_updates) { + this.render(data.body.orders_updates); + } + } catch { + return; + } + } + + public render(data: any) { + this.view.render(data); + } +} diff --git a/src/scripts/components/notice/view/csat.hbs b/src/scripts/components/notice/view/csat.hbs new file mode 100644 index 0000000..8a82a6c --- /dev/null +++ b/src/scripts/components/notice/view/csat.hbs @@ -0,0 +1,12 @@ +
+
+

×

+
+
+
+
+ +
+
+
+
\ No newline at end of file diff --git a/src/scripts/components/notice/view/csat.scss b/src/scripts/components/notice/view/csat.scss new file mode 100644 index 0000000..2b793e8 --- /dev/null +++ b/src/scripts/components/notice/view/csat.scss @@ -0,0 +1,141 @@ +@use '../../../../../public/css/parametrs/basic-mixins'; +@use '../../../../../public/css/parametrs/parameters'; + +$voting-frame-bg-color: #f4f4f4; +$voting-frame-border-color: #cccccc; +$voting-frame-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); +$voting-frame-transition: 0.3s ease-in-out; +$voting-frame-color-primary: #6a0dad; +$voting-frame-color-gray: #8c8c8c; + +#voting-frame__title__text { + font-family: 'Ubuntu', sans-serif; + font-weight: 700; +} + +.voting-frame { + position: fixed; + bottom: 1rem; + left: 1rem; + background: $voting-frame-bg-color; + border: 1px solid $voting-frame-border-color; + box-shadow: $voting-frame-shadow; + border-radius: 0.5rem; + overflow: hidden; + width: 450px; + max-width: 90%; + display: flex; + flex-direction: column; + z-index: 900; + transition: transform $voting-frame-transition, opacity $voting-frame-transition; + opacity: 1; + justify-content: space-between; + align-items: center; + + &__title { + font-size: 1rem; + margin: 0; + font-weight: 400; + align-self: flex-start; + justify-content: space-between; + margin-bottom: 5px; + + &__close { + position: absolute; + top: 10px; + right: 15px; + cursor: pointer; + font-size: 24px; + color: parameters.$text-color; + transition: color parameters.$short-duration ease; + + &:hover { + color: black; + } + } + } + + &__content { + padding: 1rem; + display: flex; + flex-direction: column; + align-items: center; + min-width: 306px; + opacity: 1; + transition: opacity 0.3s ease, transform 0.3s ease; + + &--hidden { + opacity: 0; + transform: scale(0.95); + pointer-events: none; + } + } + + &__vote-button { + @include basic-mixins.checkout-btn(); + width: 262px; + text-align: center; + font-size: 14px; + padding: 15px 0; + transition: background-color 0.2s ease; + + &:hover { + background-color: $voting-frame-color-primary; + } + } + + #back { + flex: 0 0 auto; + } +} + +@for $i from 0 through 9 { + .rating-circles { + &__circle-#{$i} { + background: hsl((120 / 10) * $i, 100%, 50%); + } + } +} + +.order-status { + margin-bottom: 10px; + font-size: 14px; + + &__highlight { + color: $voting-frame-color-primary; + font-weight: bold; + } +} + +@media (max-width: 350px) { + .voting-frame { + bottom: 0.5rem; + width: calc(100% - 20px); + max-width: none; + + margin: 0 auto; + left: 0; + right: 0; + } + + .voting-frame__title { + font-size: 0.9rem; + } + + .rating-circles__labels span { + font-size: 12px; + } + + .voting-frame__vote-button { + font-size: 12px; + padding: 10px 0; + } + .voting-frame__content { + min-width: calc(100% - 20px); + margin-top: 7px; + } + + .order-status { + font-size: 12px; + } +} \ No newline at end of file diff --git a/src/scripts/components/notice/view/csat.ts b/src/scripts/components/notice/view/csat.ts new file mode 100644 index 0000000..d1b1cd1 --- /dev/null +++ b/src/scripts/components/notice/view/csat.ts @@ -0,0 +1,88 @@ +import Handlebars from 'handlebars'; +import csatTemplate from './csat.hbs' + +export interface csatViewInterface { + render(id: string, status: string): void; + switchOrderStatus(status: string): string; +} + +export class csatView implements csatViewInterface { + private readonly compile: HandlebarsTemplates; + private readonly title: HTMLElement; + private readonly text: HTMLElement; + private container: HTMLElement | null; + + constructor(containerId: string) { + this.compile = Handlebars.compile(csatTemplate) + + this.container = document.getElementById(containerId); + + this.container.innerHTML = this.compile({title: '', text: ''}) + + this.title = document.getElementById('voting-frame__title__text'); + this.text = document.getElementById('content'); + + const backButton = document.getElementById('voting-frame__back'); + const closeButton = document.getElementById('frame__title__close'); + + backButton.addEventListener('click', (data) => { + this.container.style.display = 'none'; + }); + + closeButton?.addEventListener('click', () => { + this.container.style.display = 'none'; + }); + } + + public switchOrderStatus(status: string): string { + switch (status) { + case 'awaiting_payment': + return 'Ожидает оплаты'; + case 'paid': + return 'Оплачен'; + case 'delivered': + return 'Доставлен'; + case 'cancelled': + return 'Отменен'; + default: + return ''; + } + } + + public render = (data_in: any) => { + let text: string = ''; + let count: number = 0; + + for (const key in data_in) { + text += `\n
Статус заказа ${data_in[key].order_id} изменился на "${this.switchOrderStatus(data_in[key].new_status)}"
`; + count += 1; + } + + const data = { + "title": (count === 1) ? `Обновление заказа` : `Обновления заказов`, + "text": text.trim(), + }; + + // Style container for mobile responsiveness + this.container.style.display = 'flex'; + this.container.style.maxHeight = '400px'; + this.container.style.overflowY = 'auto'; + this.container.style.borderRadius = '10px'; + this.container.style.padding = '10px'; + + // Adjust for mobile screens + if (window.innerWidth <= 768) { + + } + + this.title.innerHTML = data.title; + this.text.innerHTML = data.text; + + // Ensure content does not overflow and IDs break correctly + this.text.style.wordWrap = 'break-word'; + const orderIds = this.text.querySelectorAll('.order-id'); + orderIds.forEach((id) => { + id.style.wordBreak = 'break-all'; + }); + } +} \ No newline at end of file diff --git a/src/scripts/components/order-list/presenters/order-list.ts b/src/scripts/components/order-list/presenters/order-list.ts index 3ae6624..341a039 100644 --- a/src/scripts/components/order-list/presenters/order-list.ts +++ b/src/scripts/components/order-list/presenters/order-list.ts @@ -17,7 +17,7 @@ export class OrderListPresenter { this.orderData = await this.orderListApi.getOrderData() this.orderListView.render(this.orderData); } catch (error) { - console.error('Не удалось инициализировать заказ', error) + // console.error('Не удалось инициализировать заказ', error) } } } \ No newline at end of file diff --git a/src/scripts/components/order-list/views/order-list.hbs b/src/scripts/components/order-list/views/order-list.hbs index d2de55b..bc419ae 100644 --- a/src/scripts/components/order-list/views/order-list.hbs +++ b/src/scripts/components/order-list/views/order-list.hbs @@ -1,25 +1,33 @@

Мои заказы

- {{#each orders}} -
-
-
- Заказ от  {{formatDate order_date}} - Доставим  {{formatDate delivery_date}} - {{status}}  {{total_price}}₽ + {{#if orders.length}} + {{#each orders}} +
+
+
+ Заказ от  {{formatDate order_date}} + Доставим  {{formatDate delivery_date}} + {{status}}  {{total_price}}₽ -
+
-
- {{#each products}} -
- {{title}} +
+ {{#each products}} +
+ {{title}} +
+ {{/each}}
- {{/each}} +
+ {{/each}} + {{else}} +
+ Crying Man +

Пока что у вас нет заказов...

+

Начните покупки, чтобы видеть их здесь!

-
- {{/each}} + {{/if}}
diff --git a/src/scripts/components/order-list/views/order-list.scss b/src/scripts/components/order-list/views/order-list.scss index 4390a0a..25bee20 100644 --- a/src/scripts/components/order-list/views/order-list.scss +++ b/src/scripts/components/order-list/views/order-list.scss @@ -1,9 +1,18 @@ @use "../../../../../public/css/parametrs/parameters"; +@use "../../../../../public/css/parametrs/basic-mixins"; .order-list { max-width: 1081px; margin: auto; + @include basic-mixins.placeholder; + + &__placeholder__icon { + width: 90px; + height: auto; + margin-bottom: 20px; + } + &__title { font-size: parameters.$big-font-size; text-align: left; diff --git a/src/scripts/components/order-list/views/order-list.ts b/src/scripts/components/order-list/views/order-list.ts index 88dd02d..71ec629 100644 --- a/src/scripts/components/order-list/views/order-list.ts +++ b/src/scripts/components/order-list/views/order-list.ts @@ -17,7 +17,7 @@ export class OrderListView { public render(data: Order[]) { this.rootElement = document.getElementById(this.rootId); if (!this.rootElement) { - console.error(`Root element with id ${this.rootId} not found`); + // console.error(`Root element with id ${this.rootId} not found`); return; } diff --git a/src/scripts/components/order-placement/api/config.ts b/src/scripts/components/order-placement/api/config.ts index 166bb1d..05ff5e4 100644 --- a/src/scripts/components/order-placement/api/config.ts +++ b/src/scripts/components/order-placement/api/config.ts @@ -27,6 +27,13 @@ export const ORDER_PLACEMENT_URLS = { 'Content-Type': 'application/json', } }, + getCartProductsWithPromocode: { + route: '/cart/select/products?promocode=', + method: 'GET', + headers: { + 'Content-Type': 'application/json', + } + }, updatePaymentMethod: { route: '/cart/pay-method', method: 'PATCH', diff --git a/src/scripts/components/order-placement/api/order-placement.ts b/src/scripts/components/order-placement/api/order-placement.ts index 84a3e4d..bad4a1c 100644 --- a/src/scripts/components/order-placement/api/order-placement.ts +++ b/src/scripts/components/order-placement/api/order-placement.ts @@ -21,11 +21,32 @@ export class OrderPlacementApiInterface { } }) .catch(err => { - console.error('Ошибка получения данных: ', err); + // console.error('Ошибка получения данных: ', err); throw err; }); } + /** + * Получение данных о продуктах в корзине с применением промокода. + * + * @returns {Promise} Возвращает данные корзины. + */ + static async getCartProductsWithPromocode(promocode: string): Promise { + return getWithCred(`${backurl}${ORDER_PLACEMENT_URLS.getCartProductsWithPromocode.route}${promocode}`) + .then(res => { + switch (res.status) { + case 200: + return this.transformCartData(res.body) as OrderData; + default: + throw new Error(`${res.status} - ${res.body.error_message}`); + } + }) + .catch(err => { + // console.error('Ошибка получения данных: ', err); + throw err; + }); + } + /** * Обновление выбранного метода оплаты. * @@ -43,7 +64,7 @@ export class OrderPlacementApiInterface { } }) .catch(error => { - console.error('Ошибка:', error); + // console.error('Ошибка:', error); throw error; }); } @@ -54,12 +75,12 @@ export class OrderPlacementApiInterface { * @async * @returns {Promise} Промис без возвращаемого значения. */ - public static async placeOrder(address: string): Promise { - console.log(address); + public static async placeOrder(address: string, promo: string): Promise { + // console.log(address); - return csrf.post(`${backurl}${ORDER_PLACEMENT_URLS.placeOrder.route}`, { address: address }) + return csrf.post(`${backurl}${ORDER_PLACEMENT_URLS.placeOrder.route}`, { address: address, promocode: promo }) .then(res => { - console.log(res.status, res.body); + // console.log(res.status, res.body); switch (res.status) { case 200: @@ -69,7 +90,7 @@ export class OrderPlacementApiInterface { } }) .catch(err => { - console.error('Ошибка при отправке заказа:', err); + // console.error('Ошибка при отправке заказа:', err); }); } @@ -107,6 +128,7 @@ export class OrderPlacementApiInterface { url: item.url, })), })), + promoStatus: data.promo_status }; } } diff --git a/src/scripts/components/order-placement/view/elements/right-element-of-order-placement/right-element-of-order-placement.hbs b/src/scripts/components/order-placement/view/elements/right-element-of-order-placement/right-element-of-order-placement.hbs index 41e92aa..c5db476 100644 --- a/src/scripts/components/order-placement/view/elements/right-element-of-order-placement/right-element-of-order-placement.hbs +++ b/src/scripts/components/order-placement/view/elements/right-element-of-order-placement/right-element-of-order-placement.hbs @@ -4,10 +4,16 @@ Всего: {{totalItems}} {{pluralize totalItems 'товар' 'товара' 'товаров'}} {{totalWeight}} кг


- + +

Промокоды

+
+ + +
+

Промокод не найден. Проверьте правильность ввода.

К оплате - {{finalPrice}} {{currency}} + {{finalPrice}} {{currency}}

diff --git a/src/scripts/components/order-placement/view/elements/right-element-of-order-placement/right-element-of-order-placement.scss b/src/scripts/components/order-placement/view/elements/right-element-of-order-placement/right-element-of-order-placement.scss index 6ce118f..d8b55aa 100644 --- a/src/scripts/components/order-placement/view/elements/right-element-of-order-placement/right-element-of-order-placement.scss +++ b/src/scripts/components/order-placement/view/elements/right-element-of-order-placement/right-element-of-order-placement.scss @@ -1,4 +1,6 @@ @use "../../../../../../../public/css/parametrs/parameters"; +@use "../../../../../../../public/css/parametrs/basic-mixins"; +@use "../../../../../../../public/css/parametrs/animations"; // Блок для карточки (корневая обертка) .right-element-card { @@ -46,7 +48,7 @@ border-radius: parameters.$card-border-radius; color: parameters.$text-color; padding: parameters.$chrckout-padding; - width: parameters.$width-max; + width: 78%; height: parameters.$right-order-height; font-size: parameters.$input-font-size; padding-left: parameters.$padding15; @@ -54,6 +56,14 @@ &::placeholder { color: parameters.$delete-color; } + + @media (max-width: 600px) { + width: 100%; + } + + @media (min-width: 1350px) { + width: 100%; + } } &__payment-method { @@ -104,6 +114,65 @@ font-weight: normal; margin-bottom: parameters.$mini-gap; } + + &__promo-container { + display: flex; + align-items: center; + gap: parameters.$main-gap; + + @media (max-width: 600px) { + flex-direction: column; + align-items: stretch; + } + + @media (min-width: 1350px) { + flex-direction: column; + align-items: stretch; + } + } + + &__apply-promo-btn { + @include basic-mixins.checkout-btn; + flex-shrink: 0; + width: 20%; + height: 40px; + padding: 0; + + @media (max-width: 600px) { + width: 100%; + margin-top: parameters.$mini-gap; + } + + @media (min-width: 1350px) { + width: 100%; + margin-top: parameters.$mini-gap; + } + } + + &__promo { + justify-content: space-around; + display: flex; + + @media (max-width: 600px) { + display: contents; + } + + @media (min-width: 1350px) { + display: contents; + } + } + + &__promo-error { + display: none; + color: parameters.$error-color; + font-size: parameters.$error-font-size; + margin-top: parameters.$mini-gap; + animation: fade-in 0.3s ease-in-out; + + &.visible { + display: block; + } + } } // Блок для кнопки @@ -139,3 +208,16 @@ } } } + +#original-price { + position: absolute; + top: 0; + left: 0; + transition: opacity 0.3s ease-in-out, transform 0.3s ease-in-out; + white-space: nowrap; /* Чтобы текст не переносился */ +} + +.right-element-card__promo-title { + margin-bottom: 10px; + font-size: 16px; +} diff --git a/src/scripts/components/order-placement/view/elements/right-element-of-order-placement/right-element-of-order-placement.ts b/src/scripts/components/order-placement/view/elements/right-element-of-order-placement/right-element-of-order-placement.ts index 828cb06..68b51ea 100644 --- a/src/scripts/components/order-placement/view/elements/right-element-of-order-placement/right-element-of-order-placement.ts +++ b/src/scripts/components/order-placement/view/elements/right-element-of-order-placement/right-element-of-order-placement.ts @@ -1,6 +1,7 @@ import {OrderPlacementApiInterface} from "../../../api/order-placement"; import { router } from '../../../../../../services/app/init'; import { add } from 'husky'; +import {OrderPlacementBuilder} from "../../order-placement-builder"; /** * Класс для управления элементами правой части страницы оформления заказа. @@ -16,6 +17,8 @@ export class RightElementOfOrderPlacementView { */ private paymentMethods: NodeListOf; + private builder: OrderPlacementBuilder; + /** * Конструктор класса RightElementOfOrderPlacementView. * Инициализирует список способов оплаты и запускает процессы инициализации. @@ -23,7 +26,9 @@ export class RightElementOfOrderPlacementView { constructor(address: string) { // Инициализация: получаем все элементы способов оплаты this.paymentMethods = document.querySelectorAll('.right-element-card__payment-method'); - console.log(address); + // console.log(address); + + this.builder = new OrderPlacementBuilder(); this.init(address); } @@ -80,15 +85,93 @@ export class RightElementOfOrderPlacementView { this.addEventListeners(); document.getElementById('order-button')?.addEventListener('click', async () => { - console.log(address); + // console.log(address); - OrderPlacementApiInterface.placeOrder(address) + OrderPlacementApiInterface.placeOrder(address, document.getElementById('apply-promo-input')?.value) .then(() => { router.navigate('/order_list'); }) .catch(err => { - console.error('что-то пошло не так', err); + // console.error('что-то пошло не так', err); }) }); + + document.getElementById('apply-promo-button')?.addEventListener('click', async () => { + + const promo = document.getElementById('apply-promo-input')?.value; + + if (promo) { + const prev = await document.getElementById('final-price')?.textContent; + const data = await OrderPlacementApiInterface.getCartProductsWithPromocode(promo); + + await this.reconstructRightPart(data); + + if (data.promoStatus !== '') { + this.showError(); + } else { + this.applyCorrectPromoAnimation(data, prev); + } + } else { + this.showError(); + } + }) + } + + private debounceTimeout: number; + + private showError () { + const err = document.getElementById('promo-error'); + + clearTimeout(this.debounceTimeout); // Очистка предыдущего таймера + + err.style.display = "flex"; + + this.debounceTimeout = setTimeout(() => { + err.style.display = "none"; + }, 3000); + } + + private async reconstructRightPart (data: any): Promise { + const finalPrice = document.getElementById('final-price'); + finalPrice.innerHTML = data.finalPrice + ' ' + data.currency; + } + + private applyCorrectPromoAnimation(data: any, prev: number): void { + const finalPrice = document.getElementById('final-price'); + const originalPrice = document.createElement('span'); + + originalPrice.id = "original-price"; + originalPrice.textContent = prev; + originalPrice.style.textDecoration = "line-through"; + originalPrice.style.marginRight = "10px"; + originalPrice.style.opacity = "1"; + originalPrice.style.transition = "opacity 0.3s ease-in-out, transform 0.3s ease-in-out"; + originalPrice.style.position = "absolute"; + originalPrice.style.transform = "translateX(0)"; + + const parent = finalPrice.parentElement; + parent.style.position = "relative"; + + parent.insertBefore(originalPrice, finalPrice); + + const finalPriceRect = finalPrice.getBoundingClientRect(); + originalPrice.style.left = `${finalPriceRect.left - parent.getBoundingClientRect().left - originalPrice.offsetWidth - 10}px`; + originalPrice.style.top = "0"; + + setTimeout(() => { + originalPrice.style.opacity = "0"; + originalPrice.style.transform = "translateX(-20px)"; + + setTimeout(() => { + originalPrice.remove(); + finalPrice.innerHTML = data.finalPrice + ' ' + data.currency; + finalPrice.style.opacity = "0"; + finalPrice.style.transition = "opacity 0.3s ease-in-out"; + + setTimeout(() => { + finalPrice.style.opacity = "1"; + }, 50); + }, 300); + }, 3000); } } diff --git a/src/scripts/components/order-placement/view/order-placement-builder.ts b/src/scripts/components/order-placement/view/order-placement-builder.ts index ba0a033..7f2043e 100644 --- a/src/scripts/components/order-placement/view/order-placement-builder.ts +++ b/src/scripts/components/order-placement/view/order-placement-builder.ts @@ -140,7 +140,7 @@ export class OrderPlacementBuilder { await this.renderLeftPart(); this.initializeOrderPlacement(); } catch (error) { - console.error(error); + // console.error(error); } } @@ -222,7 +222,7 @@ export class OrderPlacementBuilder { this.rightElementsView = new RightElementOfOrderPlacementView(this.orderData.recipient.address); } catch { - console.error('что-то не так'); + // console.error('что-то не так'); } } } diff --git a/src/scripts/components/personal-account/api/personal-account.ts b/src/scripts/components/personal-account/api/personal-account.ts index d58f5e2..c4d0309 100644 --- a/src/scripts/components/personal-account/api/personal-account.ts +++ b/src/scripts/components/personal-account/api/personal-account.ts @@ -10,12 +10,7 @@ export interface UserData { age: number; avatar_url: string; Address: { - id: number; - city: string; - street: string; - house: string; - flat: string; - profile_id: number; + address: string; }; } @@ -54,7 +49,7 @@ export class AccountAPI { const data = response.body; - console.log(data, data.delivery_date); + // console.log(data, data.delivery_date); const deliveryDate = new Date(data.delivery_date); return `Ожидаемая дата доставки: ${deliveryDate.toLocaleDateString('ru-RU', { diff --git a/src/scripts/components/personal-account/presenters/account.ts b/src/scripts/components/personal-account/presenters/account.ts index c4ad7f3..09d868c 100644 --- a/src/scripts/components/personal-account/presenters/account.ts +++ b/src/scripts/components/personal-account/presenters/account.ts @@ -6,14 +6,14 @@ import { backurl } from '../../../../services/app/config'; import { storageUser } from '../../../../services/storage/user'; import { updateAfterAuth } from '../../../layouts/body'; import { csrf } from '../../../../services/api/CSRFService'; - +import { ChangePasswordModal } from '../../modal/presenters/change-password'; export class AccountPresenter { private accountAPI: AccountAPI; private view: AccountView; - private userData: UserData; - private deliveryInfo: Array; - private rightColumnInfo: Array; + private userData: UserData | undefined; + private deliveryInfo: Array | undefined; + private rightColumnInfo: Array | undefined; constructor(apiBaseUrl: string, rootId: string) { this.accountAPI = new AccountAPI(apiBaseUrl); @@ -22,6 +22,15 @@ export class AccountPresenter { this.view.onEditAvatarClick = this.handleEditAvatar.bind(this); this.view.onEditUserInfoClick = this.handleEditUserInfo.bind(this); this.view.onEditAddressClick = this.handleEditAddress.bind(this); + this.view.onChangePasswordClick = this.changePassword.bind(this); + } + + + private changePassword() { + const changePasswordModal = new ChangePasswordModal( + { modal: 'change-password-modal', rootId: 'modal-render', btnOpen: 'change-password' }, + console.log, + ); } public async initialize() { @@ -46,16 +55,14 @@ export class AccountPresenter { } private async buildDeliveryInfo(userData: UserData) { - const addressText = [ - userData.Address?.city?.trim(), - userData.Address?.street?.trim(), - userData.Address?.house?.trim(), - userData.Address?.flat?.trim() - ].filter(Boolean).join(', ') || 'Добавьте адресс'; + const addressText = + [ + userData.Address?.address?.trim(), + ] + .filter(Boolean) + .join(', ') || 'Добавьте адрес'; - let msg: string; - - msg = await this.accountAPI.getNearestDeliveryDate() + const msg = await this.accountAPI.getNearestDeliveryDate(); return [ { @@ -92,9 +99,9 @@ export class AccountPresenter { detailsClass: 'account__favorites-details', titleClass: 'account__favorites-title', textClass: 'account__favorites-text', - title: 'Избранное', - text: 'скоро', - href: '/soon' + title: 'Вишлисты', + text: 'Смотреть', + href: '/wishlists', }, { class: 'account__purchases-info', @@ -106,7 +113,7 @@ export class AccountPresenter { title: 'Заказы', text: 'Смотреть', href: '/order_list', - } + }, ]; } @@ -122,10 +129,9 @@ export class AccountPresenter { let newAvatarUrl = await this.accountAPI.updateAvatar(file); newAvatarUrl = `${backurl}/${newAvatarUrl}`; - this.userData.avatar_url = `${backurl}/${newAvatarUrl}`; + this.userData!.avatar_url = `${backurl}/${newAvatarUrl}`; this.view.updateAvatar(newAvatarUrl); } catch (error) { - const errorMessage = this.parseError(error); this.view.displayErrorMessage(errorMessage); @@ -142,9 +148,9 @@ export class AccountPresenter { private async handleEditUserInfo() { const userDataRecord: Record = { - username: this.userData.username, - gender: this.userData.gender, - email: this.userData.email, + username: this.userData!.username, + gender: this.userData!.gender, + email: this.userData!.email, }; const userInfoModal = new PersonalDataModal( @@ -160,7 +166,7 @@ export class AccountPresenter { storageUser.changeUsername(this.userData.username); updateAfterAuth(storageUser.getUserData()); - } + }, ); userInfoModal.open(); @@ -168,23 +174,25 @@ export class AccountPresenter { private async handleEditAddress() { const addressRecord: Record = { - city: this.userData.Address.city, - street: this.userData.Address.street, - house: this.userData.Address.house, - flat: this.userData.Address.flat, + address: this.userData!.Address.address, }; const addressModal = new AddressModal( { modal: 'edit-address-modal', rootId: 'modal-render', btnOpen: 'edit-user-address' }, addressRecord, (updatedAddress) => { - this.userData.Address = { ...this.userData.Address, ...updatedAddress }; - this.view.updateAddress(this.userData.Address); + this.userData!.Address = { ...this.userData!.Address, ...updatedAddress }; + + console.log(this.userData!.Address ); + + this.view.updateAddress(this.userData!.Address.address); - storageUser.changeCity(this.userData.Address.city); + storageUser.changeCity(this.userData!.Address.address); updateAfterAuth(storageUser.getUserData()); - } + }, ); addressModal.open(); } -} \ No newline at end of file + + +} diff --git a/src/scripts/components/personal-account/views/account.ts b/src/scripts/components/personal-account/views/account.ts index 1e60b06..9399df3 100644 --- a/src/scripts/components/personal-account/views/account.ts +++ b/src/scripts/components/personal-account/views/account.ts @@ -10,6 +10,7 @@ export class AccountView { public onEditAvatarClick?: () => void; public onEditUserInfoClick?: () => void; public onEditAddressClick?: () => void; + public onChangePasswordClick?: () => void; constructor(rootId: string) { this.rootId = rootId; @@ -17,10 +18,10 @@ export class AccountView { this.compiledTemplate = Handlebars.compile(accountTemplate); } - public render(data: UserData & { deliveryInfo: Array; rightColumnInfo: Array }) { + public render(data: UserData & { deliveryInfo: Array; rightColumnInfo: Array }) { this.rootElement = document.getElementById(this.rootId); if (!this.rootElement) { - console.error(`Root element with id ${this.rootId} not found`); + // console.error(`Root element with id ${this.rootId} not found`); return; } this.rootElement.innerHTML = this.compiledTemplate(data); @@ -29,8 +30,8 @@ export class AccountView { public updateAvatar(newAvatarUrl: string) { this.rootElement = document.getElementById(this.rootId); - if (!this.rootElement){ - console.error(this.rootElement, 'not found'); + if (!this.rootElement) { + // console.error(this.rootElement, 'not found'); return; } @@ -46,46 +47,42 @@ export class AccountView { errorContainer.textContent = errorMessage; if (!this.rootElement) { - console.error('root element with id ${this.rootId} not found'); + // console.error('root element with id ${this.rootId} not found'); return; } const avatarContainer = this.rootElement.querySelector('.account__user-name') as HTMLElement | null; if (!avatarContainer) { - console.error('avatar container not found'); + // console.error('avatar container not found'); return; } - console.log(avatarContainer,errorContainer); + // console.log(avatarContainer,errorContainer); avatarContainer.appendChild(errorContainer); - console.log(avatarContainer); + // console.log(avatarContainer); setTimeout(() => { errorContainer.remove(); }, 20000); } - public updateAddress(address: UserData['Address']) { + + public updateAddress(address: string) { this.rootElement = document.getElementById(this.rootId); - if (!this.rootElement){ - console.error(this.rootElement, 'not found'); + if (!this.rootElement) { + // console.error(this.rootElement, 'not found'); return; } const addressElement = this.rootElement.querySelector('.account__address-details .account__address-text'); if (addressElement) { - addressElement.textContent = `${address.city}, ${address.street}, ${address.house}`; - if (address.flat) { - console.log(address.flat); - - addressElement.textContent += `, ${address.flat}`; - } + addressElement.textContent = `${address}`; } } private setupListeners() { this.rootElement = document.getElementById(this.rootId); - if (!this.rootElement){ - console.error(this.rootElement, 'not found'); + if (!this.rootElement) { + // console.error(this.rootElement, 'not found'); return; } @@ -109,5 +106,12 @@ export class AccountView { if (this.onEditAddressClick) this.onEditAddressClick(); }); } + + const changePasswordBtn = this.rootElement.querySelector('#change-password'); + if (changePasswordBtn) { + changePasswordBtn.addEventListener('click', () => { + if (this.onChangePasswordClick) this.onChangePasswordClick(); + }); + } } } \ No newline at end of file diff --git a/src/scripts/components/personal-account/views/personal-account.hbs b/src/scripts/components/personal-account/views/personal-account.hbs index 2ff43e6..257bbba 100644 --- a/src/scripts/components/personal-account/views/personal-account.hbs +++ b/src/scripts/components/personal-account/views/personal-account.hbs @@ -32,7 +32,6 @@
- diff --git a/src/scripts/components/personal-account/views/personal-account.scss b/src/scripts/components/personal-account/views/personal-account.scss index 7d94219..8ca0a9d 100644 --- a/src/scripts/components/personal-account/views/personal-account.scss +++ b/src/scripts/components/personal-account/views/personal-account.scss @@ -131,7 +131,7 @@ } &__address_update{ - margin-bottom: 60px; + height: 100%; } &__notification { @@ -139,12 +139,18 @@ margin-right: -170px; } + &__user-info { + display: flex; + flex-direction: row; + } + &__user-name { white-space: nowrap; overflow: visible; text-overflow: ellipsis; margin: 0; flex: 4; + font-size: 18px; } &__icons-personal-account { @@ -262,6 +268,8 @@ &__edit{ color: parameters.$base-background-color; + font-size: 18px; + margin-left: 5px; } } } diff --git a/src/scripts/components/personal-account/views/personal-account__middle.scss b/src/scripts/components/personal-account/views/personal-account__middle.scss index 4e68797..c8ea4e4 100644 --- a/src/scripts/components/personal-account/views/personal-account__middle.scss +++ b/src/scripts/components/personal-account/views/personal-account__middle.scss @@ -3,7 +3,6 @@ @media (max-width: 1000px) { .account { - // Уменьшаем размеры фотографий и текста для уменьшенных экранов &__avatar-container { width: 120px; height: 120px; diff --git a/src/scripts/components/personal-account/views/personal-account__mobile.scss b/src/scripts/components/personal-account/views/personal-account__mobile.scss index 4267081..f7a9071 100644 --- a/src/scripts/components/personal-account/views/personal-account__mobile.scss +++ b/src/scripts/components/personal-account/views/personal-account__mobile.scss @@ -55,7 +55,7 @@ } &__avatar-container:hover .account__avatar-overlay { - display: block; // Показываем подсказку при наведении + display: block; } .product-page__description-wrapper { @@ -63,7 +63,7 @@ } .product-page__description-item { - display: block; // Делает каждый элемент блока в одну строку + display: block; margin-bottom: 6px; } @@ -99,13 +99,12 @@ padding-top: 10px; } - // Показываем
только на мобильных устройствах br { - display: none; // Скрываем по умолчанию + display: none; } br.mobile-break { - display: block; // Показываем только на мобильных устройствах + display: block; } } @@ -115,11 +114,11 @@ &__pin-drop, &__favorite, &__shopping-basket { - font-size: 24px; // Уменьшаем иконки + font-size: 24px; } &__edit { - font-size: 20px; // Уменьшаем размер иконки редактирования + font-size: 20px; } } } diff --git a/src/scripts/components/product-page/api/product-page.ts b/src/scripts/components/product-page/api/product-page.ts index 63eb052..b3dcc69 100644 --- a/src/scripts/components/product-page/api/product-page.ts +++ b/src/scripts/components/product-page/api/product-page.ts @@ -1,5 +1,5 @@ import { backurl } from '../../../../services/app/config'; -import { getWithCred } from '../../../../services/api/without-csrf'; +import {get, getWithCred} from '../../../../services/api/without-csrf'; import { csrf } from '../../../../services/api/CSRFService'; interface ProductOption { @@ -43,8 +43,8 @@ interface ProductData { } export class ProductPageApi { - getProductData = (productId: string): Promise< { ok: boolean, body: ProductData }> => { - return getWithCred(`${backurl}/product/${productId}`) + getProductData = async (productId: string): Promise< { ok: boolean, body: ProductData }> => { + return await getWithCred(`${backurl}/product/${productId}`) .then((res) => { switch (res.status) { case 200: @@ -54,22 +54,22 @@ export class ProductPageApi { } }) .catch(e => { - console.error('Error fetching product data:', e); + // console.error('Error fetching product data:', e); return { ok: false }; }) } - addToCart = (productId: string): Promise<{ ok: boolean; unauthorized?: boolean }> =>{ + addToCart = async (productId: string): Promise<{ ok: boolean; unauthorized?: boolean; res?: any }> =>{ return csrf.post(`${backurl}/cart/product/${productId}`) .then((res) => { switch (res.status) { case 204: - return { ok: true }; + return { ok: true, unauthorized: false, res: res }; case 401: return { ok: false, unauthorized: true }; case 403: csrf.refreshToken() - .catch(err => console.log(err)); + .catch(err => {/*console.log(err)*/}); return { ok: false }; case 409: @@ -79,7 +79,7 @@ export class ProductPageApi { } }) .catch((error) => { - console.error('Error adding to cart:', error); + // console.error('Error adding to cart:', error); return { ok: false }; }); } @@ -95,7 +95,7 @@ export class ProductPageApi { return { ok: false, unauthorized: true }; case 403: csrf.refreshToken() - .catch(err => console.log(err)); + .catch(err => {/*console.log(err)*/}); return { ok: false }; default: @@ -103,7 +103,7 @@ export class ProductPageApi { } }) .catch((error) => { - console.error('Error removing from cart:', error); + // console.error('Error removing from cart:', error); return { ok: false }; }); } @@ -112,13 +112,13 @@ export class ProductPageApi { return csrf.patch(`${backurl}/cart/product/${productId}`, { count }) .then(res =>{ switch (res.status) { - case 204: - return; + case 200: + return res.body; case 403: csrf.refreshToken() - .catch(err => console.log(err)); + .catch(err => {/*console.log(err)*/}); - return; + return res.body; default: throw new Error(`${res.status} - ${res.body.error_message}`); } diff --git a/src/scripts/components/product-page/presenters/product-page.ts b/src/scripts/components/product-page/presenters/product-page.ts index 086ecb0..420b9d9 100644 --- a/src/scripts/components/product-page/presenters/product-page.ts +++ b/src/scripts/components/product-page/presenters/product-page.ts @@ -6,16 +6,30 @@ import { router } from '../../../../services/app/init'; import { ProductData } from '../types/types'; import { isAuth } from '../../../../services/storage/user'; import { csrf } from '../../../../services/api/CSRFService'; +import { ReviewsView } from '../../reviews/views/reviews'; +import { ReviewsPresenter } from '../../reviews/presenters/reviews'; +import { ReviewsApi } from '../../reviews/api/api'; +import { WishlistApi } from '../../wish-list/api/wish-list'; import {ReviewsView} from "../../reviews/views/reviews"; import {ReviewsPresenter} from "../../reviews/presenters/reviews"; import {ReviewsApi} from "../../reviews/api/api"; +import {Recommendations} from "../../recomendations/presenter/recommendations"; +import {RecommendationsView} from "../../recomendations/view/recomendations"; +import {CardView} from "../../card/view/card"; +import {RecommendationsApi} from "../../recomendations/api/recommendations"; +import {productData} from "./data"; export class ProductPageBuilder { private readonly reviewsId = 'reviews'; + private readonly recommendationsId = 'recommendations-page'; private productPage: ProductPage; private api = new ProductPageApi(); + private reviewsPresenter: ReviewsPresenter; + private wishlistModal: HTMLElement | null = null; + private wishlistCheckboxes: NodeListOf | null = null; private reviewsPresenter: ReviewsPresenter + private recommendations: Recommendations; constructor() { this.productPage = new ProductPage(); @@ -23,18 +37,24 @@ export class ProductPageBuilder { new ReviewsApi(); const reviewsView = new ReviewsView(this.reviewsId); this.reviewsPresenter = new ReviewsPresenter(reviewsView); + const cardView = new CardView(); + const recommendationsView = new RecommendationsView(cardView); + const recommendationsApi = new RecommendationsApi(); + this.recommendations = new Recommendations(recommendationsApi, recommendationsView); } async build({ hash }: { hash?: string }) { try { - console.log(hash); - const id = this.getProductId(); - await csrf.refreshToken(); + try { + await csrf.refreshToken(); + } catch { + + } - if (id === ''){ - router.navigate('/') + if (id === '') { + router.navigate('/'); return; } @@ -58,22 +78,153 @@ export class ProductPageBuilder { this.initializeConditionButtons(); // this.initializeOptionButtons(); - this.initializeCartButtons(productData.in_cart); + this.initializeCartButtons(productData.in_cart, productData.count); // this.initializeFavoriteIcon(); new Carousel(); - this.reviewsPresenter.init(id, hash); + this.recommendations.render(productData.id, productData.title, this.recommendationsId); + this.reviewsPresenter.init(id, hash); + this.initializeFavoriteButton(); } catch (error) { - console.error('Error building product page:', error); + // console.error('Error building product page:', error); + } + } + + private initializeFavoriteButton() { + const favoriteButton = document.querySelector('.product-page__favorite-button') as HTMLElement; + + favoriteButton?.addEventListener('click', () => { + this.openWishlistModal(); + }); + } + + private openWishlistModal() { + this.wishlistModal = document.querySelector('.modal-add-wish') as HTMLElement; + this.wishlistCheckboxes = this.wishlistModal?.querySelectorAll('.modal-add-wish__checkbox') as NodeListOf; + + if (this.wishlistModal) { + this.wishlistModal.classList.add('modal-add-wish--open'); + console.log('Modal opened'); + this.loadUserWishlists() + .then(() => this.attachModalCloseEvent()) + .catch(()=>this.attachModalCloseEvent()); + return; + } + + + this.attachModalCloseEvent(); + + } + + private closeWishlistModal() { + if (this.wishlistModal) { + this.wishlistModal.classList.remove('modal-add-wish--open'); } } + private async loadUserWishlists() { + const response = await WishlistApi.getWishlists(); + + this.populateWishlistCheckboxes(response); + + const submitButton = this.wishlistModal?.querySelector('.modal-add-wish__submit-button') as HTMLElement; + submitButton?.addEventListener('click', (event) => { + event.preventDefault(); + this.saveSelectedWishlists(); + }); + } + + + private populateWishlistCheckboxes(wishlists: any[]) { + const wishlistContainer = document.getElementById('wishlistCheckboxes'); + if (wishlistContainer) { + wishlistContainer.innerHTML = ''; // очищаем контейнер + + wishlists.forEach((wishlist) => { + const checkbox = document.createElement('input'); + checkbox.type = 'checkbox'; + checkbox.classList.add('modal-add-wish__checkbox'); + checkbox.setAttribute('data-wishlist-link', wishlist.link); // сохраняем ссылку + + const label = document.createElement('label'); + label.textContent = wishlist.name; + + const checkboxWrapper = document.createElement('div'); + checkboxWrapper.appendChild(checkbox); + checkboxWrapper.appendChild(label); + + wishlistContainer.appendChild(checkboxWrapper); + }); + } else { + console.error('Wishlist container not found'); + } + } + + + private saveSelectedWishlists() { + const wishlistContainer = document.getElementById('wishlistCheckboxes'); + if (!wishlistContainer) { + console.error('Wishlist container not found'); + return; + } + + // Получаем все выбранные чекбоксы и извлекаем их ссылки (data-wishlist-link) + const selectedWishlists = Array.from(wishlistContainer.querySelectorAll('.modal-add-wish__checkbox:checked')) + .map((checkbox) => (checkbox as HTMLInputElement).getAttribute('data-wishlist-link')); // извлекаем ссылку + + if (selectedWishlists.length === 0) { + console.log('zero'); + return; + } + + const productId = this.getProductId(); + const numId = Number(productId); + + if (!numId) { + console.error('Invalid product ID'); + return; + } + + WishlistApi.addProductToWishlist(numId, selectedWishlists) + .then((result) => { + if (result.status === 201) { + console.log('Product added to wishlist'); + this.closeWishlistModal(); + } else { + console.error('Failed to add product to wishlist'); + } + }) + .catch((error) => { + console.error('Error saving wishlists:', error); + }); + } + + private handleDocumentClick = (event: MouseEvent) => { + const target = event.target as HTMLElement; + + if (this.wishlistModal && !this.wishlistModal.contains(target)) { + this.closeWishlistModal(); + document.removeEventListener('click', this.handleDocumentClick); // Убираем обработчик после закрытия + } + }; + + + private attachModalCloseEvent() { + const closeButton = this.wishlistModal?.querySelector('.modal-add-wish__close-button') as HTMLElement; + + closeButton?.addEventListener('click', () => { + this.closeWishlistModal(); + }); + + document.addEventListener('click', this.handleDocumentClick); + } + private initializeConditionButtons() { const conditionButtons = Array.from(document.querySelectorAll('.product-page__condition-buttons button')).filter( - (el): el is HTMLButtonElement => el instanceof HTMLButtonElement + (el): el is HTMLButtonElement => el instanceof HTMLButtonElement, ); const currentPriceElement = document.querySelector('.product-page__current-price-product-page') as HTMLElement; @@ -89,7 +240,7 @@ export class ProductPageBuilder { private initializeOptionButtons() { // Обработка цветовых кнопок const colorButtons = Array.from(document.querySelectorAll('.product-page__color-button')).filter( - (el): el is HTMLButtonElement => el instanceof HTMLButtonElement + (el): el is HTMLButtonElement => el instanceof HTMLButtonElement, ); colorButtons.forEach((button) => { @@ -103,7 +254,7 @@ export class ProductPageBuilder { }); const sizeButtons = Array.from(document.querySelectorAll('.product-page__size-button')).filter( - (el): el is HTMLButtonElement => el instanceof HTMLButtonElement + (el): el is HTMLButtonElement => el instanceof HTMLButtonElement, ); sizeButtons.forEach((button) => { @@ -117,7 +268,7 @@ export class ProductPageBuilder { }); const textOptions = Array.from(document.querySelectorAll('.product-page__text-option')).filter( - (el): el is HTMLAnchorElement => el instanceof HTMLAnchorElement + (el): el is HTMLAnchorElement => el instanceof HTMLAnchorElement, ); textOptions.forEach((link) => { @@ -132,16 +283,16 @@ export class ProductPageBuilder { }); } - private getProductId = (): string =>{ + private getProductId = (): string => { const keys = router.getRouteParams(); if (keys === null) { return ''; } return keys['id']; - } + }; - private initializeCartButtons(isInCart: boolean) { + private initializeCartButtons(isInCart: boolean, count: number = 0) { const cartButton = document.querySelector('.product-page__cart-button') as HTMLButtonElement; const incrementButton = document.createElement('button'); incrementButton.textContent = '+'; @@ -149,7 +300,7 @@ export class ProductPageBuilder { incrementButton.style.display = isInCart ? 'inline-block' : 'none'; if (isInCart) { - cartButton.textContent = 'Удалить из корзины'; + cartButton.textContent = `Удалить из корзины (${count})`; this.productPage.setButtonPressedState(cartButton); } else { if (!isAuth()) { @@ -180,38 +331,37 @@ export class ProductPageBuilder { private async addToCart(cartButton: HTMLElement, incrementButton: HTMLElement) { const id = this.getProductId(); - if (id === ''){ - router.navigate('/') + if (id === '') { + router.navigate('/'); return; } - this.api.addToCart(id) - .then((result) => { - if (result.unauthorized) { - cartButton.textContent = 'Войдите в аккаунт'; - cartButton.setAttribute('router', 'changed-active'); - cartButton.setAttribute('href', '/login'); - return; - } + const result = await this.api.addToCart(id) - if (result.ok) { - cartButton.textContent = 'Удалить из корзины'; - incrementButton.style.display = 'inline-block'; - this.productPage.setButtonPressedState(cartButton); - } - }); + if (result.unauthorized) { + cartButton.textContent = 'Войдите в аккаунт'; + cartButton.setAttribute('router', 'changed-active'); + cartButton.setAttribute('href', '/login'); + return; + } + + if (result.ok) { + cartButton.textContent = `Удалить из корзины (1)`; + incrementButton.style.display = 'inline-block'; + this.productPage.setButtonPressedState(cartButton); + } } private async removeFromCart(cartButton: HTMLElement, incrementButton: HTMLElement) { const id = this.getProductId(); - if (id === ''){ - router.navigate('/') + if (id === '') { + router.navigate('/'); return; } this.api.rmFromCart(id) .then((result) => { - console.log(result); + // console.log(result); if (result.unauthorized) { cartButton.textContent = 'Войдите в аккаунт'; @@ -225,7 +375,7 @@ export class ProductPageBuilder { incrementButton.style.display = 'none'; this.productPage.setButtonDefaultState(cartButton); } - }); + }); } private async increaseCartCount(cartButton: HTMLElement, incrementButton: HTMLElement) { @@ -235,10 +385,11 @@ export class ProductPageBuilder { } try { - await ProductPageApi.updateProductQuantity(id); - cartButton.textContent = 'Удалить из корзины'; + const count = await ProductPageApi.updateProductQuantity(id); + + cartButton.textContent = `Удалить из корзины (${count.count})`; } catch (error) { - console.error('Ошибка при обновлении количества:', error); + // console.error('Ошибка при обновлении количества:', error); } } diff --git a/src/scripts/components/product-page/views/new-pr-page.hbs b/src/scripts/components/product-page/views/new-pr-page.hbs index 8ecdaab..ffa4bf6 100644 --- a/src/scripts/components/product-page/views/new-pr-page.hbs +++ b/src/scripts/components/product-page/views/new-pr-page.hbs @@ -1,5 +1,19 @@ + + +
- {{> carousel-slider images=images }} + {{> carousel-slider images=images }}

{{title}}

@@ -16,77 +30,53 @@ {{rating}} - {{formatNumber review_count}} {{pluralize review_count 'отзыв' 'отзыва' 'отзывов'}} - - + {{formatNumber review_count}} {{pluralize + review_count 'отзыв' 'отзыва' 'отзывов'}}
brand logo {{seller.name}}
- - - - - - - - - - - - - - - - - - - - -
    {{#each characteristics}}
    - {{@key}}: {{this}} + {{@key}}: {{this}}
    {{/each}}
- - - - -
-
{{price}} ₽
+
+
{{price}} ₽
+ +
+
{{original_price}} ₽
- - - +
- - - - - -
-
-

Описание

-
{{description}}
+

Описание

+
{{description}}
+
+
-
\ No newline at end of file + + + diff --git a/src/scripts/components/product-page/views/product-page.scss b/src/scripts/components/product-page/views/product-page.scss index a25aa13..333af63 100644 --- a/src/scripts/components/product-page/views/product-page.scss +++ b/src/scripts/components/product-page/views/product-page.scss @@ -136,7 +136,7 @@ } & .text-review { - margin-left: parameters.$margin-left-material-icons; + margin-left: 45px; } & span { @@ -247,8 +247,8 @@ margin-bottom: parameters.$big-margin-bottom; font-size: 18px; font-weight: 440; - margin-top: parameters.$small-margin; - margin-left: 10px; + margin-top: 10px; + margin-left: 20px; } &__description-item { @@ -287,6 +287,7 @@ word-break: normal; overflow-wrap: break-word; hyphens: auto; + margin-left: 0.5px; &__header { font-size: 25px; @@ -298,7 +299,7 @@ &__purchase-info { flex: 0.75; - width: 20%; + width: 300px; margin-left: parameters.$big-margin; } @@ -314,6 +315,7 @@ padding: parameters.$big-margin; margin-bottom: parameters.$big-margin; position: relative; + width: 300px; } &__current-price-product-page { @@ -421,7 +423,12 @@ width: 2/3 * 100%; } -@media screen and (max-width: 768px) { +.recommendations-page { + width: 2/3 * 100%; + margin-left: 20px; +} + +@media screen and (max-width: 1150px) { .product { flex-direction: column; padding: 10px; @@ -470,6 +477,7 @@ margin-bottom: 10px; } + margin-left: 0.5px; font-size: 14px; padding: 10px; } @@ -525,11 +533,16 @@ &__slider { width: 100% !important; } + + &__price-box { + width: 100% !important; + } } .product-page__description-product-page { font-size: 14px; - margin-top: 15px; + margin-top: 10px; + margin-left: 20px; padding: 10px; } @@ -540,8 +553,209 @@ #reviews { width: 100%; } + + .recommendations-page { + width: 100%; + margin-right: 0; + margin-left: 0; + } +} + +.recommendations__catalog { + margin-right: 10px; + margin-left: 10px; +} + +.cards-view-title { + transition: max-height 0.3s ease-in-out, white-space 0.5s ease-in-out; + max-height: 1.5em; + + &:hover { + white-space: normal; + max-height: 10em; + } +} + +@media screen and (max-width: parameters.$smallest-phone) { + .cards-view-title { + transition: max-height 0.3s ease-in-out, white-space 0.5s ease-in-out; + max-height: 10em; + white-space: normal !important; + + &:hover { + white-space: normal; + max-height: 10em; + } + } +} +.modal-add-wish { + display: none; /* Скрыть модалку по умолчанию */ + position: fixed; + z-index: 100; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.4); + padding-top: 60px; + + &__content { + background-color: #fefefe; + margin: 5% auto; + padding: 20px; + border: 1px solid #888; + width: 55%; + border-radius: 20px; + } + + &__header { + font-size: 1.5rem; + margin-bottom: 20px; + } + + &__checkboxes { + display: flex; + flex-direction: column; + margin-bottom: 20px; + + label{ + font-size: 20px; + padding-left: 12px; + margin-bottom: 5px; + } + } + + &__form { + display: flex; + flex-direction: column; + } + + &__submit-button, + &__close-button { + padding: 10px; + margin: 10px; + cursor: pointer; + background-color: parameters.$base-background-color; + border-radius: 20px; + color: white; + border: none; + font-size: 1rem; + + } + + &__close-button { + background-color: #ccc; + color: black; + font-weight: 600; + } + + &__submit-button:hover { + background-color: #4b0e8c; + font-weight: 600; + } + + &__close-button:hover { + background-color: #999; + } + + &--active { + display: block; + } +} + + +.product-page__price-and-favorite { + display: flex; // Используем flexbox для размещения элементов в одну строку + align-items: center; // Центрируем элементы по вертикали + gap: 10px; // Расстояние между ценой и кнопкой +} + +.product-page__current-price-product-page { + font-size: 1.5rem; + font-weight: bold; +} + +.product-page__favorite-button { + background: none; + border: none; + cursor: pointer; + padding: 0; + display: flex; + align-items: center; // Центрируем SVG внутри кнопки +} + +.product-page__favorite-button svg { + width: 24px; + height: 24px; + stroke: #ff4040; } +.modal-add-wish { + display: none; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + align-items: center; + justify-content: center; +} +.modal-add-wish--open { + display: flex; +} + +.modal-add-wish__checkbox { + position: relative; + width: 24px; /* Увеличенный размер чекбокса */ + height: 24px; + appearance: none; /* Убираем стандартный чекбокс */ + -webkit-appearance: none; + -moz-appearance: none; + border: 2px solid #6a5acd; /* Фиолетовая рамка */ + border-radius: 5px; /* Скругленные края */ + outline: none; + cursor: pointer; + background: #fff; + transition: all 0.3s ease; /* Плавный переход */ +} +.modal-add-wish__checkbox:checked { + background: #6a5acd; /* Заполнение фиолетовым цветом */ + border-color: #6a5acd; +} + +.modal-add-wish__checkbox:checked::after { + content: '✔'; /* Галочка */ + position: absolute; + top: 2px; + left: 4px; + font-size: 16px; + color: #fff; /* Цвет галочки */ + transition: all 0.2s ease; /* Плавное появление */ +} + +/* Эффект при наведении */ +.modal-add-wish__checkbox:hover { + border-color: #9370db; /* Светлее фиолетовый */ + box-shadow: 0 0 10px rgba(106, 90, 205, 0.5); /* Тень вокруг */ +} + +/* Анимация при клике */ +.modal-add-wish__checkbox:active { + transform: scale(0.9); /* Небольшое сжатие */ +} + +.recommendations__catalog { + grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)) !important; +} + +.recommendations__catalog .product-page__rating { + justify-content: center; +} + +.recommendations__catalog .catalog__wrap { + display: contents; +} diff --git a/src/scripts/components/product-page/views/product-view.ts b/src/scripts/components/product-page/views/product-view.ts index b62b33d..6a0bc3d 100644 --- a/src/scripts/components/product-page/views/product-view.ts +++ b/src/scripts/components/product-page/views/product-view.ts @@ -11,9 +11,10 @@ export class ProductPage { this.rootElement = document.getElementById(rootId); if (!this.rootElement) { - console.error(`Элемент root ${rootId} не найден`); + // console.error(`Элемент root ${rootId} не найден`); return; } + Handlebars.registerPartial('carousel-slider', carouselSliderTemplate); this.rootElement.innerHTML = ''; const templateElement = document.createElement('div'); diff --git a/src/scripts/components/recomendations/api/config.ts b/src/scripts/components/recomendations/api/config.ts new file mode 100644 index 0000000..fb77694 --- /dev/null +++ b/src/scripts/components/recomendations/api/config.ts @@ -0,0 +1,7 @@ +export const RECOMMENDATIONS_URLS = { + GET_PRODUCTS: { + route: '/product/', + name: 'Главная oxic', + REG_EXP:new RegExp(`^/product/$`), + }, +}; diff --git a/src/scripts/components/recomendations/api/recommendations.ts b/src/scripts/components/recomendations/api/recommendations.ts new file mode 100644 index 0000000..c4348f1 --- /dev/null +++ b/src/scripts/components/recomendations/api/recommendations.ts @@ -0,0 +1,38 @@ +import { getWithCred } from '../../../../services/api/without-csrf'; +import { backurl } from '../../../../services/app/config'; +import { Product } from '../../card/api/card'; +import {RECOMMENDATIONS_URLS} from "./config"; + +export interface RecommendationsApiInterface { + getProducts(id: number): Promise; +} + +export class RecommendationsApi implements RecommendationsApiInterface { + constructor() { + + } + + getProducts = (id: number): Promise => { + return getWithCred(backurl + RECOMMENDATIONS_URLS.GET_PRODUCTS.route + id + '/recommendations') + .then((res) => { + return res.body as Product[]; + }) + .catch(() => { + return null; + }); + }; + + // getProducts = async (id: number): Promise => { + // return Array.from({ length: 20 }, (_, index) => ({ + // id: index + 1, + // title: `Product ${index + 1}`, + // description: `Description for product ${index + 1}`, + // price: 1000 + index * 10, + // originalPrice: 1200 + index * 10, + // discount: 200, + // count: 100, + // rating: 4.5, + // image_url: `https://via.placeholder.com/150?text=Product+${index + 1}`, + // })); + // }; +} \ No newline at end of file diff --git a/src/scripts/components/recomendations/presenter/recommendations.ts b/src/scripts/components/recomendations/presenter/recommendations.ts new file mode 100644 index 0000000..f60be0c --- /dev/null +++ b/src/scripts/components/recomendations/presenter/recommendations.ts @@ -0,0 +1,97 @@ +import { RecommendationsApiInterface } from '../api/recommendations'; +import { RecommendationsViewInterface } from '../view/recomendations'; +import {backurl, rootId} from '../../../../services/app/config'; +import { router } from '../../../../services/app/init'; +import { DropdownConfig, DropdownPresenter } from '../../dropdown-btn/presenter/dropdown'; +import { DropdownAPI } from '../../dropdown-btn/api/dropdown'; + + +export class Recommendations { + private api: RecommendationsApiInterface; + private view: RecommendationsViewInterface; + private dropdownPresenter: DropdownPresenter; + private apiEndpoint = '/product/'; + private readonly config: DropdownConfig; + private readonly id: number | null = null; + private readonly name: string | null = null; + + constructor(api: RecommendationsApiInterface, view: RecommendationsViewInterface) { + this.api = api; + this.view = view; + + this.config = { + containerId: 'sort-container', + sortOptions: [ + { value: 'price_asc', text: 'Сначала дешевле' }, + { value: 'price_desc', text: 'Сначала дороже'}, + { value: 'rating', text: 'По рейтингу' }, + ], + apiEndpoint: this.apiEndpoint, + defaultSort: 'cost', + defaultOrder: 'asc', + onSortChange: this.handleSortChange, + }; + } + + private handleSortChange = (sortOrder: string): void => { + if (!this.id || !this.name) return; + + // Маппинг значений sortOrder в параметры sort и order + let sort: string; + let order: string; + + switch (sortOrder) { + case 'price_asc': + sort = 'price'; + order = 'asc'; + break; + case 'price_desc': + sort = 'price'; + order = 'desc'; + break; + default: + sort = 'rating'; + order = 'desc'; + break; + } + + router.navigate(`${this.apiEndpoint}/${this.id}/recommendations?sort=${sort}&order=${order}`); + + this.recommendationsProducts(this.id, this.name, sort, order); + }; + + async render(id: number, name: string, rootId: string) { + this.id = id; + this.name = name; + + const products = {products: await this.api.getProducts(this.id)}; + + if (products.products.length > 6) { + //products.more = true; + products.products = products.products.slice(0, 6); + } + + this.view.renderProducts(products, 'Похожее на ' + this.name, this.config, rootId, false, false, this.apiEndpoint + this.id + '/recommendations' + `?id=${this.id}&title=${this.name}`); + } + + public recommendationsProducts(id: number, name: string, sort: string, order: string) { + DropdownAPI.sortProducts(`${backurl}${this.apiEndpoint}/${id}/recommendations`, sort, order) + .then(async (productsApi) => { + let products; + + if (productsApi !== undefined) { + products = productsApi.body; + products.forEach((card: any) => { + card.image_url = `${backurl}/${card.image_url}`; + }); + } else { + products = {}; + } + + await this.view.renderProducts({products: products}, 'Похожее на ' + name, this.config, rootId, false, false); + }) + .catch(() => { + return + }); + } +} \ No newline at end of file diff --git a/src/scripts/components/recomendations/view/recomendations.ts b/src/scripts/components/recomendations/view/recomendations.ts new file mode 100644 index 0000000..eb92e12 --- /dev/null +++ b/src/scripts/components/recomendations/view/recomendations.ts @@ -0,0 +1,35 @@ +import Handlebars from 'handlebars'; +import { Product } from '../../card/api/card'; +import { CardViewInterface } from '../../card/view/card'; +import { DropdownConfig, DropdownPresenter } from '../../dropdown-btn/presenter/dropdown'; +import {backurl, rootId} from "../../../../services/app/config"; +import {b} from "vite/dist/node/types.d-aGj9QkWt"; + +export interface RecommendationsViewInterface { + renderProducts(data: { products: Product[] , page_title?: string}, query: string, config: DropdownConfig, rootId?: string, show?: boolean, flag?: boolean, url?: string): void; +} + +export class RecommendationsView implements RecommendationsViewInterface { + private readonly cardView: CardViewInterface; + + constructor(cardView: CardViewInterface) { + this.cardView = cardView; + } + + public renderProducts = (data: { products: Product[] , page_title?: string}, query: string, config: DropdownConfig, newRootId: string = rootId, show: boolean = true, flag: boolean = false, url: string = '') => { + data.page_title = ''; + + data.products.forEach((pr) => { + pr.image_url = `${backurl}/${pr.image_url}`; + }); + + this.cardView.render(data, query, newRootId, show, flag, url); + + document.querySelectorAll('.catalog').forEach((catalog) => { + catalog.classList.add('recommendations__catalog'); + }); + + const dropdownPresenter = new DropdownPresenter(config); + dropdownPresenter.initView(); + }; +} \ No newline at end of file diff --git a/src/scripts/components/reviews/presenters/reviews.ts b/src/scripts/components/reviews/presenters/reviews.ts index de52749..8652af7 100644 --- a/src/scripts/components/reviews/presenters/reviews.ts +++ b/src/scripts/components/reviews/presenters/reviews.ts @@ -33,10 +33,10 @@ export class ReviewsPresenter { this.loadReviews(id) .then(() => { if (hash) { - console.log(hash); + // console.log(hash); const targetElement = document.getElementById(hash); - console.log(targetElement); + // console.log(targetElement); if (targetElement) { targetElement.scrollIntoView({ behavior: 'smooth' }); @@ -67,28 +67,24 @@ export class ReviewsPresenter { })) }; + console.log(reviewsData); + this.view.render(id, formattedReviews); const view = new AddReviewView(); new AddReviewPresenter(id, view, this.loadReviews); }) .catch((err: Error) => { - console.error(123, err); - - if (err.message === 'Ошибка при загрузке отзывов: 404'){ - const formattedReviews = { - total_review_count: 0, - total_review_rating: 0, - reviews: [] - }; - - this.view.render(id, formattedReviews); - const view = new AddReviewView(); - new AddReviewPresenter(id, view, this.loadReviews); + const formattedReviews = { + total_review_count: 0, + total_review_rating: 0, + reviews: [] + }; - return; - } + this.view.render(id, formattedReviews); + const view = new AddReviewView(); + new AddReviewPresenter(id, view, this.loadReviews); - this.view.renderError('Не удалось загрузить отзывы. Попробуйте позже.'); + return; }); }; } diff --git a/src/scripts/components/reviews/views/reviews-card.hbs b/src/scripts/components/reviews/views/reviews-card.hbs index d877420..6685d15 100644 --- a/src/scripts/components/reviews/views/reviews-card.hbs +++ b/src/scripts/components/reviews/views/reviews-card.hbs @@ -2,7 +2,7 @@
- +
diff --git a/src/scripts/components/reviews/views/reviews.hbs b/src/scripts/components/reviews/views/reviews.hbs index 7e9a966..94310f2 100644 --- a/src/scripts/components/reviews/views/reviews.hbs +++ b/src/scripts/components/reviews/views/reviews.hbs @@ -44,7 +44,7 @@
+ this.src='/assets/img/static/avatar.svg'" alt="">
diff --git a/src/scripts/components/reviews/views/reviews.ts b/src/scripts/components/reviews/views/reviews.ts index e0e1398..5dcfcd7 100644 --- a/src/scripts/components/reviews/views/reviews.ts +++ b/src/scripts/components/reviews/views/reviews.ts @@ -69,13 +69,13 @@ export class ReviewsView implements ReviewsViewInterface { const rootElement = document.getElementById(this.rootId); if (!rootElement) { - console.error(`Ошибка: rootElement не найден, rootId: ${this.rootId}`); + // console.error(`Ошибка: rootElement не найден, rootId: ${this.rootId}`); return; } // Проверка структуры данных if (!Array.isArray(data.reviews)) { - console.error('Ошибка: data.reviews должен быть массивом, получено:', data.reviews); + // console.error('Ошибка: data.reviews должен быть массивом, получено:', data.reviews); data.reviews = []; } @@ -91,7 +91,7 @@ export class ReviewsView implements ReviewsViewInterface { const user = storageUser.getUserData(); - console.log('nen', user); + // console.log('nen', user); // Генерация шаблона с проверкой и передачей параметров templateElement.innerHTML = this.compiled({ @@ -180,7 +180,7 @@ export class ReviewsView implements ReviewsViewInterface { const reviewListElement = document.getElementById('review-list'); if (!reviewListElement) { - console.error('Ошибка: Элемент списка отзывов (review-list) не найден.'); + // console.error('Ошибка: Элемент списка отзывов (review-list) не найден.'); return; } @@ -190,7 +190,7 @@ export class ReviewsView implements ReviewsViewInterface { if (!data.reviews || data.reviews.length === 0) { reviewListElement.innerHTML = ''; // Очищаем содержимое - console.log('Нет отзывов для отображения.'); + // console.log('Нет отзывов для отображения.'); return; } @@ -226,7 +226,7 @@ export class ReviewsView implements ReviewsViewInterface { button.addEventListener("click", () => { const isExpanded = card.style.maxHeight === "none"; - console.log("card: ", card) + // console.log("card: ", card) card.style.maxHeight = isExpanded ? `${maxVisibleHeight}px` : "none"; button.innerHTML = isExpanded @@ -247,11 +247,11 @@ export class ReviewsView implements ReviewsViewInterface { */ loadSortedReviews = async (id: string, sortBy: string, sortOrder: string) => { try { - console.log('data: ', sortBy, sortOrder) + // console.log('data: ', sortBy, sortOrder) const user = storageUser.getUserData(); - console.log('nen', user); + // console.log('nen', user); const response = await ReviewsApi.fetchReviews(id, sortBy, sortOrder) .then((reviewsData: any) => { @@ -272,12 +272,12 @@ export class ReviewsView implements ReviewsViewInterface { }; }) .catch((err: Error) => { - console.error(err); + // console.error(err); this.renderError('Не удалось загрузить отзывы. Попробуйте позже.'); }); this.rerenderList(id, response); } catch (error) { - console.error('Ошибка при загрузке отсортированных отзывов:', error); + // console.error('Ошибка при загрузке отсортированных отзывов:', error); this.renderError('Не удалось загрузить отзывы. Попробуйте позже.'); } }; @@ -289,7 +289,7 @@ export class ReviewsView implements ReviewsViewInterface { renderError = (errorMessage: string): void => { const rootElement = document.getElementById(this.rootId); if (!rootElement) { - console.log(`Ошибка: rootElement ${rootElement} -- rootId ${this.rootId}`); + // console.log(`Ошибка: rootElement ${rootElement} -- rootId ${this.rootId}`); return; } diff --git a/src/scripts/components/searcher/api/search.ts b/src/scripts/components/searcher/api/search.ts index 7cf697d..e8adb62 100644 --- a/src/scripts/components/searcher/api/search.ts +++ b/src/scripts/components/searcher/api/search.ts @@ -15,7 +15,7 @@ export class SearcherApi implements SearcherApiInterface { return res.body as Product[]; }) .catch((e) => { - console.error(e); + // console.error(e); return null; }); }; @@ -30,12 +30,12 @@ export class SearcherApi implements SearcherApiInterface { if (res.status === 200) { return res.body.suggestions.map((suggestion: { title: string }) => suggestion.title) as Array; } else { - console.error(`Failed to fetch suggestions - ${res.status} - ${query} - ${backurl}/search?${param.toString()}`, res.body); + // console.error(`Failed to fetch suggestions - ${res.status} - ${query} - ${backurl}/search?${param.toString()}`, res.body); return null; } }) .catch(e => { - console.error('Error fetching suggestions:', e); + // console.error('Error fetching suggestions:', e); return null; }); }; diff --git a/src/scripts/components/searcher/presenter/search.ts b/src/scripts/components/searcher/presenter/search.ts index c8fc30d..f3136e7 100644 --- a/src/scripts/components/searcher/presenter/search.ts +++ b/src/scripts/components/searcher/presenter/search.ts @@ -64,7 +64,7 @@ export class Searcher { DropdownAPI.sortProducts(`${backurl}${this.apiEndpoint}?q=${query}`, sort, order) .then((productsApi) => { - console.log(productsApi); + // console.log(productsApi); const products = productsApi.body; products.forEach((card) => { card.image_url = `${backurl}/${card.image_url}`; @@ -74,7 +74,7 @@ export class Searcher { this.view.renderProducts({products: products}, query, this.config); }) .catch((error) => { - console.error('Ошибка сортировки:', error); + // console.error('Ошибка сортировки:', error); }); } }; @@ -85,7 +85,7 @@ export class Searcher { const suggestionsList = document.getElementById('suggestions') as HTMLUListElement; if (!searchInput || !searchButton|| !suggestionsList) { - console.error('Search input or button not found'); + // console.error('Search input or button not found'); return; } @@ -154,7 +154,7 @@ export class Searcher { public searchProducts(query: string, sort: string, order: string) { const searchUrl = `${backurl}${this.apiEndpoint}?q=${query}&sort=${sort}&order=${order}`; - console.log(sort, order); + // console.log(sort, order); this.api .getProductsByQuery(searchUrl) @@ -168,11 +168,11 @@ export class Searcher { this.view.renderProducts({ products }, query, this.config); this.updateBreadcrumbs(`Результаты поиска: ${query}`, `/search/catalog?q=${query}&sort=${sort}&order=${order}`); } else { - console.error('No products found'); + // console.error('No products found'); } }) .catch((e) => { - console.error('Error fetching products:', e); + // console.error('Error fetching products:', e); }); } @@ -188,7 +188,7 @@ export class Searcher { this.view.displaySuggestions([]); } }) - .catch(e => console.error('Error fetching suggestions:', e)); + .catch(e => {/*console.error('Error fetching suggestions:', e)*/}); } else { this.view.displaySuggestions([]); } @@ -196,11 +196,11 @@ export class Searcher { } private updateBreadcrumbs = (label: string, url: string) => { - console.log(`Updating breadcrumbs with label: ${label} and url: ${url}`); + // console.log(`Updating breadcrumbs with label: ${label} and url: ${url}`); }; private hideSuggestions() { - console.log(111111); + // console.log(111111); this.view.displaySuggestions([]); } diff --git a/src/scripts/components/searcher/view/search.ts b/src/scripts/components/searcher/view/search.ts index 2d3fdb5..810ac1c 100644 --- a/src/scripts/components/searcher/view/search.ts +++ b/src/scripts/components/searcher/view/search.ts @@ -30,7 +30,7 @@ export class SearcherView implements SearcherViewInterface { const rootElement = document.getElementById(rootId); if (!rootElement) { - return console.error(`Элемент ID = ${rootId} не найден`); + return // console.error(`Элемент ID = ${rootId} не найден`); } rootElement.innerHTML = ''; diff --git a/src/scripts/components/searcher/view/searcher.scss b/src/scripts/components/searcher/view/searcher.scss index 0fba973..728f3c3 100644 --- a/src/scripts/components/searcher/view/searcher.scss +++ b/src/scripts/components/searcher/view/searcher.scss @@ -41,35 +41,3 @@ cursor: pointer; } } - -.suggestions { - position: absolute; - top: 100%; - left: 0; - width: 100%; - margin-top: 2px; - background-color: white; - border-radius: 10px; - z-index: 10; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); - max-height: calc(7 * 40px + 15px); - overflow-y: auto; - - &--active { - display: block; - } - - &__item { - padding: 15px; - cursor: pointer; - - &--link { - text-decoration: none; - color: inherit; - } - - &:hover { - background-color: #f0f0f0; - } - } -} diff --git a/src/scripts/components/searcher/view/suggestions.scss b/src/scripts/components/searcher/view/suggestions.scss new file mode 100644 index 0000000..d2cc919 --- /dev/null +++ b/src/scripts/components/searcher/view/suggestions.scss @@ -0,0 +1,35 @@ +@use '../../../../../public/css/parametrs/parameters'; + +.suggestions { + position: absolute; + top: 100%; + left: 0; + width: 100%; + margin-top: 2px; + background-color: white; + border-radius: 10px; + z-index: 10; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + max-height: calc(7 * 40px + 15px); + overflow-y: auto; + + &--active { + display: block; + } + + &__item { + padding: 15px; + cursor: pointer; + + + + &--link { + text-decoration: none; + color: inherit; + } + + &:hover { + background-color: #f0f0f0; + } + } +} diff --git a/src/scripts/components/single-order/api/single-order.ts b/src/scripts/components/single-order/api/single-order.ts index 9c20c39..cae9496 100644 --- a/src/scripts/components/single-order/api/single-order.ts +++ b/src/scripts/components/single-order/api/single-order.ts @@ -13,7 +13,6 @@ export class SingleOrderApiInterface { })); const totalItems = products.reduce((sum, product) => sum + product.count, 0); - const totalPrice = products.reduce((sum, product) => sum + product.cost * product.count, 0); return { id: data.id, @@ -22,7 +21,7 @@ export class SingleOrderApiInterface { address: data.address, recipient: data.recipient, totalItems: totalItems, - totalPrice: totalPrice, + totalPrice: data.total_price, products: products, }; } @@ -47,7 +46,7 @@ export class SingleOrderApiInterface { .then(res => { switch (res.status) { case 200: - console.log(res.body); + // console.log(res.body); return SingleOrderApiInterface.transformSingleOrderData(res.body); default: @@ -55,7 +54,7 @@ export class SingleOrderApiInterface { } }) .catch(err => { - console.error('Ошибка: ', err); + // console.error('Ошибка: ', err); throw err; }) } diff --git a/src/scripts/components/single-order/presenter/single-order.ts b/src/scripts/components/single-order/presenter/single-order.ts index 5ceb2c0..2ef3704 100644 --- a/src/scripts/components/single-order/presenter/single-order.ts +++ b/src/scripts/components/single-order/presenter/single-order.ts @@ -25,7 +25,7 @@ export class SingleOrderPresenter { this.orderData = await this.singleOrderApi.getOrderData(orderId); this.singleOrderView.render(this.orderData); } catch (error) { - console.error('Не удалось инициализировать заказ', error); + // console.error('Не удалось инициализировать заказ', error); } } } diff --git a/src/scripts/components/single-order/view/single-order.ts b/src/scripts/components/single-order/view/single-order.ts index 0ab13a2..fe52d3a 100644 --- a/src/scripts/components/single-order/view/single-order.ts +++ b/src/scripts/components/single-order/view/single-order.ts @@ -18,7 +18,7 @@ export class SingleOrderView { this.rootElement = document.getElementById(this.rootId); if (!this.rootElement) { - console.error(`Root element with id ${this.rootId} not found`); + // console.error(`Root element with id ${this.rootId} not found`); return; } @@ -40,7 +40,7 @@ export class SingleOrderView { if (productId) { router.navigate(`/product/${productId}`); } else { - console.error('Product ID не найден'); + // console.error('Product ID не найден'); } }); }); diff --git a/src/scripts/components/wish-list/api/wish-list.ts b/src/scripts/components/wish-list/api/wish-list.ts new file mode 100644 index 0000000..bad1db3 --- /dev/null +++ b/src/scripts/components/wish-list/api/wish-list.ts @@ -0,0 +1,198 @@ +import { apiResponse } from '../../../../services/api/utils'; +import { csrf } from '../../../../services/api/CSRFService'; +import { backurl } from '../../../../services/app/config'; +import { getWithCred } from '../../../../services/api/without-csrf'; + +export class WishlistApi { + static async getWishlists() { + try { + const response = await getWithCred(`${backurl}/wishlists`); + + if (response.status === 404 || response.status === 400) { + return []; + } + + if (!response.status) { + throw new Error(`Ошибка при получении списка вишлистов: ${response.status}`); + } + + console.log(response.body); + + return await response.body; + } catch (error) { + console.error('Ошибка при получении списка вишлистов:', error); + // В случае любой другой ошибки возвращаем пустой массив + return []; + } + } + + static async getWishlist(link: string) { + const response = await getWithCred(`${backurl}/wishlist/${link}`); + if (response.status === 400) { + return []; + } + + return response.body; + } + + /** + * Создаёт новый вишлист. + * @param name Название вишлиста. + */ + static async createWishlist(name: string): Promise { + await csrf.refreshToken() + + return csrf.post(`${backurl}/wishlists`, { 'name': name }); + } + + /** + * Удаляет вишлист. + * @param link Уникальная ссылка на вишлист. + */ + static async deleteWishlist(link: string): Promise { + const body = { link }; + await csrf.refreshToken() + + return csrf.delete(`${backurl}/wishlists`, body); + } + + /** + * Переименовывает вишлист. + * @param link Уникальная ссылка на вишлист. + * @param newName Новое название вишлиста. + */ + static async renameWishlist(link: string, newName: string): Promise { + const body = { link, new_name: newName }; + await csrf.refreshToken() + + return csrf.patch(`${backurl}/wishlist`, body); + } + + /** + * Добавляет продукт в указанные вишлисты. + * @param productId ID продукта. + * @param links Ссылки на вишлисты. + */ + static async addProductToWishlist(productId: number, links: string[]): Promise { + const body = { product_id: productId, links }; + await csrf.refreshToken() + + return csrf.post(`${backurl}/wishlists/product`, body); + } + + /** + * Удаляет продукт из указанных вишлистов. + * @param productId ID продукта. + * @param links Ссылки на вишлисты. + */ + static async removeFromWishlist(productId: number, links: string[]): Promise { + const body = { product_id: productId, links }; + await csrf.refreshToken() + + return csrf.delete(`${backurl}/wishlists/product`, body); + } + + /** + * Копирует вишлист. + * @param link Ссылка на вишлист. + */ + static async copyWishlist(link: string): Promise { + const body = { link }; + await csrf.refreshToken() + + return csrf.post(`${backurl}/wishlist`, body); + } +} + + +export class WishlistApiMock { + async deleteItemFromWishlist(link: string, itemName: string) { + + + return Promise.resolve(); + } + + async createWishlist(name: string) { + console.log(`Создан вишлист с именем "${name}"`); + return Promise.resolve({ + id: 'new-id', + link: 'new-unique-link', + name, + lastOrderImage: 'https://via.placeholder.com/100', + }); + } + + + async getWishlists() { + return [ + { + id: '1', + link: '123e4567-e89b-12d3-a456-426614174000', + name: 'Праздничный список', + lastOrderImage: 'https://via.placeholder.com/100', + }, + { + id: '2', + link: '123e4567-e89b-12d3-a456-426614174001', + name: 'Новый год 2024', + lastOrderImage: 'https://via.placeholder.com/100', + }, + { + id: '1', + link: '123e4567-e89b-12d3-a456-426614174000', + name: 'Праздничный список', + lastOrderImage: 'https://via.placeholder.com/100', + }, + { + id: '2', + link: '123e4567-e89b-12d3-a456-426614174001', + name: 'Новый год 2024', + lastOrderImage: 'https://via.placeholder.com/100', + }, + { + id: '1', + link: '123e4567-e89b-12d3-a456-426614174000', + name: 'Праздничный список', + lastOrderImage: 'https://via.placeholder.com/100', + }, + { + id: '2', + link: '123e4567-e89b-12d3-a456-426614174001', + name: 'Новый год 2024', + lastOrderImage: 'https://via.placeholder.com/100', + }, + ]; + } + + async getWishlist(link: string) { + return { + name: `Вишлист 111`, + items: [ + { name: 'Ноутбук ASUS', image: 'https://via.placeholder.com/200', price: 2999 }, + { name: 'Наушники Sony', image: 'https://via.placeholder.com/200', price: 1999 }, + { name: 'Смартфон Samsung', image: 'https://via.placeholder.com/200', price: 3999 }, + { name: 'Ноутбук ASUS', image: 'https://via.placeholder.com/200', price: 2999 }, + { name: 'Наушники Sony', image: 'https://via.placeholder.com/200', price: 1999 }, + { name: 'Смартфон Samsung', image: 'https://via.placeholder.com/200', price: 3999 }, + { name: 'Ноутбук ASUS', image: 'https://via.placeholder.com/200', price: 2999 }, + { name: 'Наушники Sony', image: 'https://via.placeholder.com/200', price: 1999 }, + { name: 'Смартфон Samsung', image: 'https://via.placeholder.com/200', price: 3999 }, + { name: 'Ноутбук ASUS', image: 'https://via.placeholder.com/200', price: 2999 }, + { name: 'Наушники Sony', image: 'https://via.placeholder.com/200', price: 1999 }, + { name: 'Смартфон Samsung', image: 'https://via.placeholder.com/200', price: 3999 }, + + ], + }; + } + + // Пустые заглушки для удаления и обновления + async deleteWishlist(link: string) { + console.log(`Вишлист с link: ${link} удалён`); + return Promise.resolve(); + } + + async renameWishlist(link: string, name: string) { + console.log(`Вишлист с link: ${link} переименован в ${name}`); + return Promise.resolve(); + } +} diff --git a/src/scripts/components/wish-list/presenter/all-wish-list.hbs b/src/scripts/components/wish-list/presenter/all-wish-list.hbs new file mode 100644 index 0000000..8506aee --- /dev/null +++ b/src/scripts/components/wish-list/presenter/all-wish-list.hbs @@ -0,0 +1,35 @@ +
+ + +
+ +
+ {{#if wishlists.length}} + {{#each wishlists as |w|}} + + {{/each}} + {{else}} +
+ + + + +

Ваш список вишлистов пуст

+

Создайте свой первый вишлист, чтобы начать!

+
+ {{/if}} +
\ No newline at end of file diff --git a/src/scripts/components/wish-list/presenter/all-wishlists.scss b/src/scripts/components/wish-list/presenter/all-wishlists.scss new file mode 100644 index 0000000..2476f30 --- /dev/null +++ b/src/scripts/components/wish-list/presenter/all-wishlists.scss @@ -0,0 +1,203 @@ +.wishlist-container { + display: flex; + flex-wrap: wrap; + gap: 30px; + justify-content: space-between; + + + &__card { + flex: 0 0 calc(50% - 20px); + max-width: calc(50% - 20px); + background: #fff; + border: 1px solid #e0e0e0; + border-radius: 12px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + transition: transform 0.3s; + padding: 20px; + + &:hover { + transform: translateY(-5px); + } + } + + &__link { + text-decoration: none; /* Убираем подчеркивание у ссылок */ + color: inherit; /* Наследуем цвет текста */ + } + + &__content { + display: flex; + align-items: center; /* Центрируем по вертикали */ + justify-content: space-between; + width: 100%; + } + + &__title { + font-size: 24px; + font-weight: bold; + text-align: left; + flex: 1; /* Занимает доступное пространство слева */ + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + &__image { + flex-shrink: 0; /* Картинка не сжимается */ + display: flex; + align-items: center; + justify-content: center; + margin-left: 20px; + + img { + width: 100px; + height: 100px; + object-fit: cover; + border-radius: 12px; /* Закругляем углы картинки */ + } + } + + /* Мобильная версия */ + @media (max-width: 1000px) { + &__card { + flex: 0 0 100%; + max-width: 100%; + padding: 15px; + + .wishlist-container__title { + font-size: 20px; + } + + .wishlist-container__image img { + width: 80px; + height: 80px; + } + } + } +} + +.wishlist-create { + display: flex; + align-items: center; + gap: 10px; // Расстояние между полем ввода и кнопкой + margin: 20px 0; + + &__input { + flex-grow: 1; // Поле ввода занимает всю доступную ширину + padding: 10px; + border: 1px solid #ccc; + border-radius: 5px; + font-size: 14px; + outline: none; + transition: border-color 0.3s; + + &:focus { + border-color: #7a16d5; // Цвет рамки при фокусе + } + } + + &__btn { + background: #7a16d5; + color: #fff; + padding: 10px 20px; + border: none; + border-radius: 5px; + font-size: 14px; + cursor: pointer; + transition: background 0.3s; + + &:hover { + background: #5e11b0; + } + } +} + +.wishlist-container__empty { + text-align: center; + margin-top: 20px; + font-size: 16px; + color: #666; +} + +.wishlist-container { + display: flex; + flex-wrap: wrap; + gap: 30px; + justify-content: space-between; + + &__placeholder { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 100%; + padding: 40px; + text-align: center; + font-size: 18px; + font-weight: bold; + color: #666; + background-color: #f9f9f9; + border-radius: 12px; + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); + animation: fadeIn 1s ease-in-out; + } + + &__icon { + margin-bottom: 15px; + opacity: 0.7; + transition: transform 0.3s ease; + } + + &__placeholder-text, + &__placeholder-suggestion { + font-size: 16px; + color: #555; + } + + &__placeholder:hover .wishlist-container__icon { + transform: scale(1.1); + opacity: 1; + } +} + +.wishlist-container { + display: flex; + flex-wrap: wrap; + gap: 30px; + justify-content: space-between; + + &__card { + position: relative; // Для правильного позиционирования крестика + background: #fff; + border: 1px solid #e0e0e0; + border-radius: 12px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + transition: transform 0.3s; + padding: 20px; + + &:hover { + transform: translateY(-5px); + } + } + + &__delete-btn { + position: absolute; + top: 10px; + right: 10px; + background-color: transparent; + border: 2px solid red; + border-radius: 100%; + width: 24px; + height: 24px; + font-size: 18px; + font-weight: bold; + color: #e74c3c; + cursor: pointer; + transition: color 0.5s; + + &:hover { + color: #c0392b; + border: 3px solid red; + } + } +} diff --git a/src/scripts/components/wish-list/presenter/wish-list.hbs b/src/scripts/components/wish-list/presenter/wish-list.hbs new file mode 100644 index 0000000..96bad97 --- /dev/null +++ b/src/scripts/components/wish-list/presenter/wish-list.hbs @@ -0,0 +1,62 @@ +
+
+ +
+ +
+

{{name}}

+
+ + {{#if is_creator}} + + + {{/if}} +
+
+
+ + + + + +
+ {{#if items.length}} + {{#each items}} +
+
+ {{title}} + +
+
+
{{title}}
+
{{price}}
+
+
+ {{/each}} + {{else}} +
+ + + + +

Ваш вишлист пуст

+

Добавьте товары, чтобы они появились здесь!

+
+ {{/if}} +
diff --git a/src/scripts/components/wish-list/presenter/wish-list.scss b/src/scripts/components/wish-list/presenter/wish-list.scss new file mode 100644 index 0000000..0463ea6 --- /dev/null +++ b/src/scripts/components/wish-list/presenter/wish-list.scss @@ -0,0 +1,256 @@ +@use "/public/css/parametrs/parameters"; +@use "/public/css/parametrs/basic-mixins" as mixins; + +.list__wishlist { + &-header { + display: flex; + align-items: center; + margin: 50px; + justify-content: space-between; + position: relative; + + + .list__wishlist-back { + margin-right: 20px; + } + + .back-btn { + background: #ddd; + color: #000; + padding: 8px 15px; + border: none; + border-radius: 5px; + font-size: 14px; + cursor: pointer; + transition: background 0.3s ease; + + &:hover { + background: #bbb; + } + } + + .list__wishlist-back { + flex-shrink: 0; + + } + + + &__title { + font-size: 24px; + margin-bottom: 15px; + font-weight: bold; + } + + &__actions { + display: flex; + justify-content: center; + gap: 10px; + flex-wrap: wrap; + + .btn { + background: #7a16d5; + color: #fff; + padding: 8px 15px; + border: none; + border-radius: 8px; + font-size: 14px; + cursor: pointer; + transition: background 0.3s; + + &:hover { + background: #5e11b0; + } + } + } + + &__title-container { + + background: #fff; + border-radius: 12px; + padding: 20px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + text-align: center; + max-width: 600px; + width: 100%; + + position: absolute; + left: 50%; + transform: translateX(-50%); + } + } + + &-container { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 2fr)); + gap: 30px; + padding: parameters.$padding 0; + justify-content: center; + + &__card { + @include mixins.card-styles(parameters.$color-card-background, parameters.$color-border, parameters.$color-shadow, parameters.$transition-duration); + flex-direction: column; + justify-content: space-between; + align-items: center; + text-align: center; + padding: parameters.$padding; + border-radius: parameters.$default-border-radius; + cursor: pointer; + position: relative; + width: 100%; + overflow: hidden; + max-width: 350px; + transition: transform parameters.$transition-duration ease, box-shadow parameters.$transition-duration ease; + + &:hover { + transform: scale(1.05); + box-shadow: 0 4px 10px parameters.$color-shadow-hover; + z-index: parameters.$z-index-hover; + } + + &-image { + width: 100%; + height: parameters.$image-height; + border-radius: parameters.$default-border-radius; + overflow: hidden; + background-color: #f0f0f0; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: parameters.$gap; + + img { + width: 100%; + height: 100%; + object-fit: cover; + transition: transform parameters.$transition-duration ease; + } + } + + &:hover &-image img { + transform: scale(1.1); + } + + &-info { + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + text-align: center; + + &__title { + font-size: 24px; + font-weight: parameters.$font-weight-bold; + color: parameters.$color-text; + margin-bottom: parameters.$gap; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + max-width: 100%; + } + + &__description { + font-size: parameters.$font-size-description; + color: white; + margin-bottom: parameters.$gap; + } + } + } + } + + @media (max-width: 999px) { + &-container { + gap: 30px; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + padding: parameters.$padding 10px; + + &__card { + height: auto; + } + } + } + + @media (max-width: parameters.$smallest-phone) { + &__container { + grid-template-columns: 2fr; + } + } +} + +.wishlist-title-editor { + font-size: 30px; + font-weight: bold; + border: none; + border-bottom: 2px solid #7a16d5; + outline: none; + background: transparent; + padding: 0; + width: 100%; + color: inherit; +} + +.list__wishlist-container__card { + position: relative; + overflow: hidden; +} + +.delete-item-btn { + position: absolute; + top: 10px; + right: 10px; + width: 24px; + height: 24px; + border-radius: 50%; + background: none; + border: 2px solid red; + color: red; + font-size: 16px; + font-weight: bold; + line-height: 24px; + align-items: center; + display: flex; + justify-content: center; + cursor: pointer; + transition: background 0.5s ease; + + &:hover { + color: rgba(200, 0, 0, 1); + border: 3px solid rgba(200, 0, 0, 1); + } +} + +.list__wishlist-back { + margin-bottom: 20px; +} + +.list__wishlist-placeholder { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 50px; + text-align: center; + font-size: 18px; + font-weight: bold; + color: #666; + background-color: #f9f9f9; + border-radius: 12px; + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); + animation: fadeIn 0.5s ease-in-out; +} + +.list__wishlist-placeholder-icon { + margin-bottom: 15px; + opacity: 0.7; + transition: transform 0.3s ease; +} + +.list__wishlist-placeholder-text, +.list__wishlist-placeholder-suggestion { + font-size: 16px; + color: #555; +} + +.list__wishlist-placeholder:hover .list__wishlist-placeholder-icon { + transform: scale(1.1); + opacity: 1; +} diff --git a/src/scripts/components/wish-list/presenter/wish-list.ts b/src/scripts/components/wish-list/presenter/wish-list.ts new file mode 100644 index 0000000..7d1c852 --- /dev/null +++ b/src/scripts/components/wish-list/presenter/wish-list.ts @@ -0,0 +1,253 @@ +import wishlistListTemplate from './all-wish-list.hbs?raw'; +import wishlistTemplate from './wish-list.hbs?raw'; +import { router } from '../../../../services/app/init'; +import { WishlistApi } from '../api/wish-list'; +import { backurl, rootId } from '../../../../services/app/config'; +import Handlebars from 'handlebars'; + +export class WishlistPresenter { + private readonly compiledList: any; + private readonly compiledWishlist: any; + + constructor() { + this.compiledWishlist = Handlebars.compile(wishlistTemplate); + this.compiledList = Handlebars.compile(wishlistListTemplate); + } + + async renderWishlist(id: string): Promise { + const container = document.getElementById('app'); + + try { + const wishlist = await WishlistApi.getWishlist(id); + + wishlist.items.forEach(item => { + item.image_url = `${backurl}/${item.image_url}`; + }); + + console.log('123',wishlist); + + container!.innerHTML = this.compiledWishlist(wishlist); + this.addEventListeners(id); + } catch (error) { + console.error('Ошибка при загрузке вишлиста:', error); + this.showNotification('Не удалось загрузить вишлист.', 'error'); + } + } + + private addEventListeners(id: string) { + const titleElement = document.getElementById('wishlist-title') as HTMLElement; + + document.getElementById('back-btn')?.addEventListener('click', () => { + router.navigate('/wishlists'); // Перенаправляем пользователя на список вишлистов + }); + + document.getElementById('rename-btn')?.addEventListener('click', () => { + this.enableInlineEditing(titleElement, id); + }); + + document.getElementById('share-btn')?.addEventListener('click', () => { + navigator.clipboard.writeText(window.location.href); + this.showNotification('Ссылка скопирована!', 'success'); + }); + + const deleteWishlistModal = document.getElementById('delete-wishlist-modal') as HTMLElement; + const confirmDeleteWishlistBtn = document.getElementById('confirm-delete-wishlist-btn') as HTMLButtonElement; + const cancelDeleteWishlistBtn = document.getElementById('cancel-delete-wishlist-btn') as HTMLButtonElement; + + document.getElementById('delete-btn')?.addEventListener('click', () => { + this.showModal(deleteWishlistModal); + }); + + confirmDeleteWishlistBtn.addEventListener('click', async () => { + try { + await WishlistApi.deleteWishlist(id); + this.showNotification('Вишлист успешно удалён!', 'success'); + router.navigate('/wishlists'); + } catch (error) { + console.error('Ошибка при удалении вишлиста:', error); + this.showNotification('Не удалось удалить вишлист.', 'error'); + } finally { + this.hideModal(deleteWishlistModal); + } + }); + + cancelDeleteWishlistBtn.addEventListener('click', () => { + this.hideModal(deleteWishlistModal); + }); + + const modal = document.getElementById('delete-confirmation-modal') as HTMLElement; + const confirmDeleteBtn = document.getElementById('confirm-delete-btn') as HTMLButtonElement; + const cancelDeleteBtn = document.getElementById('cancel-delete-btn') as HTMLButtonElement; + + let itemNameToDelete: string | null = null; + + document.querySelectorAll('.delete-item-btn').forEach((button) => { + button.addEventListener('click', (event) => { + const itemName = (event.currentTarget as HTMLElement).getAttribute('data-item-name'); + if (itemName) { + itemNameToDelete = itemName; + this.showModal(modal); + } + }); + }); + + confirmDeleteBtn.addEventListener('click', async () => { + if (itemNameToDelete) { + try { + const link = router.getRouteParams() + const l = link!["link"] + + await WishlistApi.removeFromWishlist(Number(itemNameToDelete), [l]); + this.showNotification(`Товар "${itemNameToDelete}" успешно удалён!`, 'success'); + this.renderWishlist(id); + } catch (error) { + console.error('Ошибка при удалении товара:', error); + this.showNotification('Не удалось удалить товар.', 'error'); + } finally { + this.hideModal(modal); + itemNameToDelete = null; + } + } + }); + + cancelDeleteBtn.addEventListener('click', () => { + this.hideModal(modal); + itemNameToDelete = null; + }); + } + + private showModal(modal: HTMLElement) { + modal.classList.add('visible'); + const outsideClickListener = (event: MouseEvent) => { + if (event.target === modal) { + this.hideModal(modal); + modal.removeEventListener('click', outsideClickListener); + } + }; + modal.addEventListener('click', outsideClickListener); + } + + private hideModal(modal: HTMLElement) { + modal.classList.remove('visible'); + } + + private enableInlineEditing(element: HTMLElement, id: string): void { + const currentTitle = element.innerText; + + const input = document.createElement('input'); + input.type = 'text'; + input.value = currentTitle; + input.className = 'wishlist-title-editor'; + + const saveChanges = async () => { + const newTitle = input.value.trim(); + if (newTitle && newTitle !== currentTitle) { + try { + await WishlistApi.renameWishlist(id, newTitle); + element.innerText = newTitle; + this.showNotification('Название успешно изменено!', 'success'); + } catch (error) { + console.error('Ошибка при сохранении:', error); + this.showNotification('Не удалось сохранить изменения.', 'error'); + element.innerText = currentTitle; + } + } else { + element.innerText = currentTitle; + } + element.style.display = ''; + input.remove(); + }; + + input.addEventListener('keydown', (event) => { + if (event.key === 'Enter') { + input.blur(); + } + }); + + input.addEventListener('blur', saveChanges); + + element.innerHTML = ''; + element.style.display = 'block'; + element.appendChild(input); + + input.focus(); + } + + async renderWishlistList(): Promise { + const container = document.getElementById(rootId); + + try { + const wishlists = await WishlistApi.getWishlists(); + container!.innerHTML = this.compiledList({ wishlists }); + + document.querySelectorAll('.wishlist-link').forEach((link) => { + link.addEventListener('click', (event) => { + event.preventDefault(); + const url = (event.currentTarget as HTMLAnchorElement).getAttribute('href'); + router.navigate(url!); + }); + }); + + document.querySelectorAll('.wishlist-title').forEach((title) => { + title.addEventListener('click', async () => { + const element = title as HTMLElement; + const link = element.closest('.wishlist-link')?.getAttribute('href')?.split('/').pop(); + if (link) { + this.enableInlineEditing(element, link); + } + }); + }); + + const createButton = document.getElementById('create-wishlist-btn') as HTMLButtonElement; + const nameInput = document.getElementById('wishlist-name-input') as HTMLInputElement; + + createButton.addEventListener('click', async () => { + const name = nameInput.value.trim(); + if (!name) { + this.showNotification('Введите название для нового вишлиста.', 'warning'); + return; + } + + try { + await WishlistApi.createWishlist(name); + this.showNotification(`Вишлист "${name}" создан!`, 'success'); + await this.renderWishlistList(); + } catch (error) { + console.error('Ошибка при создании вишлиста:', error); + this.showNotification('Не удалось создать вишлист.', 'error'); + } + }); + + document.querySelectorAll('.wishlist-container__delete-btn').forEach((button) => { + button.addEventListener('click', async (event) => { + const link = (event.currentTarget as HTMLElement).getAttribute('data-id'); + if (link) { + try { + await WishlistApi.deleteWishlist(link); + this.showNotification('Вишлист успешно удалён!', 'success'); + await this.renderWishlistList(); // Перерисовываем список вишлистов + } catch (error) { + console.error('Ошибка при удалении вишлиста:', error); + this.showNotification('Не удалось удалить вишлист.', 'error'); + } + } + }); + }); + } catch (error) { + console.error('Ошибка при загрузке списка вишлистов:', error); + console.log(error); + this.showNotification('Не удалось загрузить список вишлистов.', 'error'); + } + } + + private showNotification(message: string, type: 'success' | 'error' | 'warning') { + const notification = document.createElement('div'); + notification.className = `notification ${type}`; + notification.textContent = message; + + document.body.appendChild(notification); + setTimeout(() => { + notification.remove(); + }, 3000); + } +} diff --git a/src/scripts/components/wish-list/presenter/wishlist-modal.scss b/src/scripts/components/wish-list/presenter/wishlist-modal.scss new file mode 100644 index 0000000..ff8e247 --- /dev/null +++ b/src/scripts/components/wish-list/presenter/wishlist-modal.scss @@ -0,0 +1,62 @@ +.wishlist-modal { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background: rgba(0, 0, 0, 0.5); // Полупрозрачный фон + display: flex; + justify-content: center; + align-items: center; + z-index: 5000; + opacity: 0; + pointer-events: none; + transition: opacity 0.3s ease-in-out; + + &.visible { + opacity: 1; + pointer-events: all; + } + + &-content { + background: #fff; + border-radius: 10px; + padding: 20px; + width: 300px; + text-align: center; + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2); + } + + &-actions { + display: flex; + justify-content: space-between; + margin-top: 20px; + + .btn { + padding: 10px 20px; + border: none; + border-radius: 5px; + cursor: pointer; + font-size: 14px; + transition: background 0.3s; + + &.confirm { + background: #e74c3c; + color: white; + + &:hover { + background: #c0392b; + } + } + + &.cancel { + background: #ddd; + color: black; + + &:hover { + background: #bbb; + } + } + } + } +} diff --git a/src/scripts/errors/error.ts b/src/scripts/errors/error.ts index 5486523..24cf313 100644 --- a/src/scripts/errors/error.ts +++ b/src/scripts/errors/error.ts @@ -13,7 +13,7 @@ export const errors = { * @param {Error} error - Объект ошибки, которая возникла при загрузке шаблона. */ TemplatizerError: (error: Error): void => { - console.error('Ошибка загрузки шаблона:', error); + // console.error('Ошибка загрузки шаблона:', error); }, /** @@ -24,7 +24,7 @@ export const errors = { * @param {Error} error - Объект ошибки, которая возникла при получении товаров. */ GetCardsError: (error: Error): void => { - console.error('Не удалось получить товары с сервера:', error); + // console.error('Не удалось получить товары с сервера:', error); }, /** @@ -32,7 +32,7 @@ export const errors = { * @function */ GetUsername: (): void => { - console.error('Ошибка: username не найден в ответе'); + // console.error('Ошибка: username не найден в ответе'); }, /** @@ -41,6 +41,6 @@ export const errors = { * @param {Error} error - Ошибка, возникшая при выполнении запроса. */ BadGet: (error: Error): void => { - console.error('Ошибка при выполнении запроса:', error); + // console.error('Ошибка при выполнении запроса:', error); }, }; diff --git a/src/scripts/layouts/body.hbs b/src/scripts/layouts/body.hbs index 4f0c3da..72505ca 100644 --- a/src/scripts/layouts/body.hbs +++ b/src/scripts/layouts/body.hbs @@ -10,17 +10,23 @@
+ +

-
- home - Главная +
+ favorite + Вишлисты
menu Каталог
+
+ home + Главная +
shopping_cart Корзина diff --git a/src/scripts/layouts/body.ts b/src/scripts/layouts/body.ts index e16fb3e..983b9d3 100644 --- a/src/scripts/layouts/body.ts +++ b/src/scripts/layouts/body.ts @@ -50,7 +50,7 @@ export function buildBody(data: any): Promise { handleHeaderScroll() }) .catch((err) => { - console.error(err); + // console.error(err); }); } @@ -59,12 +59,12 @@ export function buildBody(data: any): Promise { export const updateAfterAuth = (user: User): void => { const avatarElement = document.getElementById('avatar'); if (!avatarElement) { - console.error('avatarElement not found'); + // console.error('avatarElement not found'); return; } const nameElement = document.getElementById('name'); if (!nameElement) { - console.error('nameElement not found'); + // console.error('nameElement not found'); return; } @@ -85,12 +85,12 @@ export const updateAfterAuth = (user: User): void => { export const updateAfterLogout = (user: User): void => { const avatarElement = document.getElementById('avatar'); if (!avatarElement) { - console.error('avatarElement not found'); + // console.error('avatarElement not found'); return; } const nameElement = document.getElementById('name'); if (!nameElement) { - console.error('nameElement not found'); + // console.error('nameElement not found'); return; } diff --git a/src/scripts/layouts/header/header.hbs b/src/scripts/layouts/header/header.hbs index 8ebb090..3268551 100644 --- a/src/scripts/layouts/header/header.hbs +++ b/src/scripts/layouts/header/header.hbs @@ -33,9 +33,9 @@ shopping_cart Корзина
-
+
favorite - Избранное + Вишлисты
receipt_long diff --git a/src/scripts/layouts/header/header.scss b/src/scripts/layouts/header/header.scss index 20dfe5c..732ea88 100644 --- a/src/scripts/layouts/header/header.scss +++ b/src/scripts/layouts/header/header.scss @@ -282,7 +282,7 @@ header { } txt { - transform: translateY(parameters.$y-coord-min); + transform: translateY(0); } @media (max-width: 1150px) { diff --git a/src/scripts/utils/debounce.ts b/src/scripts/utils/debounce.ts new file mode 100644 index 0000000..671978d --- /dev/null +++ b/src/scripts/utils/debounce.ts @@ -0,0 +1,15 @@ + + +export const debounce = (func: (...args: any[]) => void, delay: number): (...args: any[]) => void => { + let timeoutId: NodeJS.Timeout | null = null; + +return (...args: any[]) => { + if (timeoutId) { + clearTimeout(timeoutId); // Убираем предыдущий таймер + } + + timeoutId = setTimeout(() => { + func(...args); + }, delay); +}; +} \ No newline at end of file diff --git a/src/scripts/utils/helperName.ts b/src/scripts/utils/helperName.ts index 7e36d77..b2d991a 100644 --- a/src/scripts/utils/helperName.ts +++ b/src/scripts/utils/helperName.ts @@ -74,7 +74,7 @@ const helperName: Array = [ * @returns {Promise} Возвращает true в случае успешной регистрации, * иначе false при ошибке. */ -export async function registerFunctions(): Promise { +export async function registerFunctions(): Promise { try { helperName.forEach((helpFunction: iHelper) => { Handlebars.registerHelper(helpFunction.name, helpFunction.function); @@ -84,7 +84,7 @@ export async function registerFunctions(): Promise { return (arg1 === arg2) ? options.fn(this) : options.inverse(this); }); } catch (error) { - console.error(error); + // console.error(error); return false; } diff --git a/src/services/api/CSRFService.ts b/src/services/api/CSRFService.ts index 6ad5856..a26342d 100644 --- a/src/services/api/CSRFService.ts +++ b/src/services/api/CSRFService.ts @@ -27,9 +27,9 @@ export class CSRFService { this.token = csrfToken; }) - .catch(err => { - console.error(err); - }); + // .catch(err => { + // // console.error(err); + // }); } /** @@ -40,7 +40,6 @@ export class CSRFService { */ private getRequestInfo(method: string, body: any): RequestInit | undefined { if (!this.token) { - console.log(this.token); return undefined; } @@ -65,6 +64,7 @@ export class CSRFService { private protectedFetchWithoutResponse = async (url: string, method: string, body: any): Promise => { const info = this.getRequestInfo(method, body); if (!info) { + console.log(method, body, info); return Promise.reject(new Error('Отсутствует конфигурация запроса (RequestInit)')); } diff --git a/src/services/api/utils.ts b/src/services/api/utils.ts index 80c3e06..587bba1 100644 --- a/src/services/api/utils.ts +++ b/src/services/api/utils.ts @@ -1,3 +1,5 @@ +import {c} from "vite/dist/node/types.d-aGj9QkWt"; + /** * Интерфейс, описывающий структуру ответа API. */ @@ -27,7 +29,6 @@ export const parseJsonResponse = async (res: Response): Promise => const responseJson = await res.json(); return { status: responseJson.status, body: responseJson.body }; } catch { - console.log(res); - throw new Error("не получилось распарсить в json"); + //throw new Error("не получилось распарсить в json"); } -}; \ No newline at end of file +}; diff --git a/src/services/app/app.ts b/src/services/app/app.ts index 232021e..7b2a1a0 100644 --- a/src/services/app/app.ts +++ b/src/services/app/app.ts @@ -1,12 +1,13 @@ -import { router, searcher } from './init.js'; -import { backurl, CLICK_CLASSES, rootId, urlAttribute } from './config.ts'; -import { defaultUser, storageUser } from '../storage/user'; -import { User } from '../types/types'; -import { registerFunctions } from '../../scripts/utils/helperName'; -import { categoryStorage } from '../storage/category'; -import { buildBody, updateAfterAuth, updateAfterLogout } from '../../scripts/layouts/body'; -import { get, getWithCred } from '../api/without-csrf'; -import { Category } from '../../scripts/components/category/api/category'; +import {router, searcher} from './init.js'; +import {backurl, CLICK_CLASSES, rootId, urlAttribute} from './config.ts'; +import {defaultUser, storageUser} from '../storage/user'; +import {User} from '../types/types'; +import {registerFunctions} from '../../scripts/utils/helperName'; +import {categoryStorage} from '../storage/category'; +import {buildBody, updateAfterAuth, updateAfterLogout} from '../../scripts/layouts/body'; +import {get, getWithCred} from '../api/without-csrf'; +import {Category} from '../../scripts/components/category/api/category'; +import {csatPresenter} from "../../scripts/components/notice/presenters/csat"; /** @@ -16,13 +17,13 @@ import { Category } from '../../scripts/components/category/api/category'; * @returns {Promise} Возвращает промис, который завершается после построения интерфейса. */ export const buildMain = (user: User): Promise => { - return buildBody({ rootId }).then(() => { - if (user.username === '') { - updateAfterLogout(user); - } else { - updateAfterAuth(user); - } - }); + return buildBody({rootId}).then(() => { + if (user.username === '') { + updateAfterLogout(user); + } else { + updateAfterAuth(user); + } + }); }; // if ('serviceWorker' in navigator) { @@ -44,68 +45,71 @@ export const buildMain = (user: User): Promise => { */ document.addEventListener('DOMContentLoaded', () => { - const hash = window.location.hash; - if (hash && hash === '#review') { - const reviewElement = document.getElementById('review'); - if (reviewElement) { - reviewElement.scrollIntoView({ behavior: 'smooth' }); // Плавный скролл + try { + const hash = window.location.hash; + if (hash && hash === '#review') { + const reviewElement = document.getElementById('review'); + if (reviewElement) { + reviewElement.scrollIntoView({behavior: 'smooth'}); // Плавный скролл + } + } + } catch { + } - } - return registerFunctions() - .then(() => { - getWithCred(backurl) - .then(res => { - switch (res.status) { - case 200: - return res.body as User; - case 401: - return defaultUser; - } + return registerFunctions().then(() => { + getWithCred(backurl + '/') + .then((res) => { + switch (res.status) { + case 200: + return res.body as User; + case 401: + return defaultUser; + } - throw Error(`ошибка ${res.status}`); - }) - .then(user => { - storageUser.saveUserData(user as User); + throw Error(`ошибка ${res.status}`); + }) + .then((user) => { + storageUser.saveUserData(user as User); - return user; - }) - .then(user => { - buildMain(user) - .then(() => { - router.init(); + return user; + }) + .then((user) => { + buildMain(user).then(() => { + router.init(); - let routes = document.querySelectorAll(`[router="${CLICK_CLASSES.stability}"]`); + const routes = document.querySelectorAll(`[router="${CLICK_CLASSES.stability}"]`); - routes.forEach((route) => { - let href = route.getAttribute(urlAttribute); - if (href) { - route.addEventListener('click', (event) => { - event.preventDefault(); - if (href) router.navigate(href, true); - }); - } - }); - }); + routes.forEach((route) => { + const href = route.getAttribute(urlAttribute); + if (href) { + route.addEventListener('click', (event) => { + event.preventDefault(); + if (href) router.navigate(href, true); + }); + } + }); + }); - searcher.initializeListeners(); - return user; - }) - .then(() => { - get(backurl + '/categories') - .then(res => { - if (res.status !== 200) { - throw Error('ошибка при загрузке категорий'); - } + searcher.initializeListeners(); + const csatPresener = new csatPresenter("notice") + return user; + }) + .then(() => { + get(backurl + '/categories') + .then((res) => { + if (res.status !== 200) { + //throw Error('ошибка при загрузке категорий'); + } - return res.body as Category[]; + return res.body as Category[]; + }) + .then((data) => { + categoryStorage.setCategories(data as Category[]); + }); }) - .then(data => { - categoryStorage.setCategories(data as Category[]); + .catch((err) => { + //console.error('ошибка инициализации приложения:', err); }); - }) - .catch((err) => { - console.error('ошибка инициализации приложения:', err); - }) }); }); diff --git a/src/services/app/config.ts b/src/services/app/config.ts index f029e89..e3ba216 100644 --- a/src/services/app/config.ts +++ b/src/services/app/config.ts @@ -11,8 +11,8 @@ export const rootId: string = 'app'; * @constant {string} * @default 'http://localhost:8000/' */ -export const backurl = 'http://localhost:8000'; -// export const backurl = 'http://94.139.246.241:8000'; +// export const backurl = 'http://localhost:8000/api/v1'; +export const backurl = 'https://oxic.shop/api/v1'; /** * Значение атрибуда router для элементов с кликабельными ссылками. diff --git a/src/services/app/init.ts b/src/services/app/init.ts index c3f703f..e2cf40c 100644 --- a/src/services/app/init.ts +++ b/src/services/app/init.ts @@ -28,6 +28,10 @@ import { PERSONAL_ACCOUNT } from '../../scripts/components/personal-account/conf import { SearcherApi } from '../../scripts/components/searcher/api/search'; import { SearcherView } from '../../scripts/components/searcher/view/search'; import { Searcher } from '../../scripts/components/searcher/presenter/search'; +import { WishlistPresenter } from '../../scripts/components/wish-list/presenter/wish-list'; +import {Recommendations} from "../../scripts/components/recomendations/presenter/recommendations"; +import {RecommendationsApi} from "../../scripts/components/recomendations/api/recommendations"; +import {RecommendationsView} from "../../scripts/components/recomendations/view/recomendations"; HandlebarsRegEqual(); @@ -64,6 +68,10 @@ const searcherApi = new SearcherApi(); const searcherView = new SearcherView(cardView); export const searcher = new Searcher(searcherApi, searcherView); +const recommendationsApi = new RecommendationsApi(); +const recommendationsView = new RecommendationsView(cardView); +export const recommendations = new Recommendations(recommendationsApi, recommendationsView); + router.addRoute( '/search/catalog', () => { @@ -71,8 +79,6 @@ router.addRoute( const sort = router.getQueryParam('sort') || 'price'; // Параметр сортировки по умолчанию const order = router.getQueryParam('order') || 'asc'; // Порядок сортировки по умолчанию - console.log({ query, sort, order }); - if (query) { searcher.searchProducts(query, sort, order); // Передаем параметры в searchProducts } else { @@ -84,6 +90,26 @@ router.addRoute( false, ); +// router.addRoute( +// '/product/:id/recommendations', +// () => { +// const routeParams = router.getRouteParams(); +// +// const name = router.getQueryParam('title'); +// const sort = router.getQueryParam('sort') || 'price'; // Параметр сортировки по умолчанию +// const order = router.getQueryParam('order') || 'asc'; // Порядок сортировки по умолчанию +// +// if (routeParams['id']) { +// recommendations.recommendationsProducts(routeParams['id'], name, sort, order); // Передаем параметры в searchProducts +// } else { +// router.navigate('/'); // Перенаправляем на главную, если нет запроса +// } +// }, +// new RegExp('^\\/product\\/(\\d+)\\/recommendations(\\?.*)?$'), // Обновляем RegExp для новых параметров +// false, +// false, +// ); + router.addRoute(AUTH_URLS.LOGIN.route, () => loginPresenter.init(), AUTH_URLS.LOGIN.REG_EXP, @@ -113,9 +139,9 @@ router.addRoute( '/product/:id:hash', (params) => { let hash = window.location.hash; // Извлекаем текущий хэш - console.log('Параметры маршрута:', params, 'Хэш:', hash); + //console.log('Параметры маршрута:', params, 'Хэш:', hash); if (hash) hash = hash.replace(/^#/, ''); - productPageBuilder.build({ hash }).catch(e => console.error(e)); + productPageBuilder.build({ hash }).catch(e => {/*console.error(e)*/}); }, new RegExp('^\\/product\\/(\\d+)(#.*)?$'), // Регулярное выражение с поддержкой хэша false, @@ -187,4 +213,32 @@ router.addRoute( new RegExp('^/category/([^/]+)$'), false, false +); + +const wishlistPresenter = new WishlistPresenter(); + +router.addRoute( + '/wishlists', + () => wishlistPresenter.renderWishlistList(), + new RegExp('^/wishlists$'), + false, + false +); + +router.addRoute( + '/wishlist/:link', + () => { + const routeParams = router.getRouteParams(); + if (!routeParams) { + router.navigate('/wishlists'); + return; + } + + const link = routeParams['link']; + + wishlistPresenter.renderWishlist(link).finally(); + }, + new RegExp('^\\/wishlist\\/([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$'), + false, + false ); \ No newline at end of file diff --git a/src/services/app/router.ts b/src/services/app/router.ts index 7a76ce4..f7ed571 100644 --- a/src/services/app/router.ts +++ b/src/services/app/router.ts @@ -136,10 +136,10 @@ export default class Router { */ private addHandlers = () => { const checkDOMAndAddListeners = () => { - let routes = document.querySelectorAll(`[router="${CLICK_CLASSES.overrideable}"]`); + const routes = document.querySelectorAll(`[router="${CLICK_CLASSES.overrideable}"]`); routes.forEach((route) => { - let href = route.getAttribute(urlAttribute); + const href = route.getAttribute(urlAttribute); if (href && !route.hasAttribute('data-listener-added')) { route.addEventListener('click', (event) => { event.preventDefault(); @@ -218,10 +218,10 @@ export default class Router { * Удаляет обработчики событий с элементов маршрутов. */ private removeHandlers() { - let routes = document.querySelectorAll(`[router="${CLICK_CLASSES.overrideable}"]`); + const routes = document.querySelectorAll(`[router="${CLICK_CLASSES.overrideable}"]`); routes.forEach((route) => { - let href = route.getAttribute(urlAttribute); + const href = route.getAttribute(urlAttribute); if (href) { route.removeEventListener('click', (event) => { event.preventDefault(); diff --git a/src/services/storage/user.ts b/src/services/storage/user.ts index a0abd7c..561f668 100644 --- a/src/services/storage/user.ts +++ b/src/services/storage/user.ts @@ -38,7 +38,7 @@ class StorageUser { * @param data - Объект IUser с новыми данными пользователя. */ public saveUserData(data: IUser): void { - console.log(data); + // console.log(data); this.userData = data; } diff --git a/sw.ts b/sw.ts index 6827a9b..6043730 100644 --- a/sw.ts +++ b/sw.ts @@ -1,47 +1,46 @@ -// const CACHE_NAME = 'cache-v2'; -// const DYNAMIC_CACHE_NAME = 'dynamic'; -// -// // List of URLs to precache -// const PRECACHE_URLS: string[] = [ -// '/', -// '/catalog', -// '/files', -// ]; -// -// self.addEventListener('install', (event: any) => { -// event.waitUntil( -// caches.open(CACHE_NAME).then((cache) => { -// return cache.addAll(PRECACHE_URLS); -// }) -// ); -// }); -// -// self.addEventListener('fetch', (event: any) => { -// event.respondWith( -// caches.match(event.request).then((cachedResponse) => { -// if (cachedResponse) { -// return cachedResponse; -// } -// return fetch(event.request).then((networkResponse) => { -// return caches.open(DYNAMIC_CACHE_NAME).then((cache) => { -// cache.put(event.request, networkResponse.clone()); -// return networkResponse; -// }); -// }); -// }) -// ); -// }); -// -// self.addEventListener('activate', (event: any) => { -// event.waitUntil( -// caches.keys().then((cacheNames) => { -// return Promise.all( -// cacheNames.map((cacheName) => { -// if (cacheName !== CACHE_NAME && cacheName !== DYNAMIC_CACHE_NAME) { -// return caches.delete(cacheName); -// } -// }) -// ); -// }) -// ); -// }); +const CACHE_NAME = 'cache-v2'; +const DYNAMIC_CACHE_NAME = 'dynamic'; + +// List of URLs to precache. +const PRECACHE_URLS: string[] = [ + '/', + '/catalog', + '/files', +]; + +self.addEventListener('install', (event: any) => { + event.waitUntil( + caches.open(CACHE_NAME).then((cache) => { + return cache.addAll(PRECACHE_URLS); + }) + ); +}); + +self.addEventListener('fetch', (event: any) => { + event.respondWith( + caches.match(event.request).then((cachedResponse) => { + if (cachedResponse) { + return cachedResponse; + } + return fetch(event.request).then((networkResponse) => { + return caches.open(DYNAMIC_CACHE_NAME).then((cache) => { + cache.put(event.request, networkResponse.clone()); + return networkResponse; + }); + }); + })); +}); + +self.addEventListener('activate', (event: any) => { + event.waitUntil( + caches.keys().then((cacheNames) => { + return Promise.all( + cacheNames.map((cacheName) => { + if (cacheName !== CACHE_NAME && cacheName !== DYNAMIC_CACHE_NAME) { + return caches.delete(cacheName); + } + }) + ); + }) + ); +}); diff --git a/vite.config.ts b/vite.config.ts index 73cdd59..496a899 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -3,7 +3,7 @@ import { VitePWA } from 'vite-plugin-pwa'; import { resolve } from 'path'; import handlebars from 'vite-plugin-handlebars'; import string from 'vite-plugin-string'; -import fs from 'fs'; +import fs from "fs"; import path from 'path'; import { fileURLToPath } from 'url'; @@ -54,14 +54,14 @@ export default defineConfig({ sourcemap: true, rollupOptions: { input: { - main: path.resolve(__dirname, 'index.html'), // Явно указываем index.html - ...templates, // Добавляем все найденные шаблоны + main: path.resolve(__dirname, 'index.html'), + ...templates, }, external: [resolve(__dirname, 'services/app/app.ts')], }, }, server: { - port: 3000, // Порт для dev-сервера + port: 3000, }, preview: { // host: '0.0.0.0', @@ -69,7 +69,7 @@ export default defineConfig({ }, resolve: { alias: { - '@': resolve(__dirname, 'src'), // Правильное задание алиаса для пути + '@': resolve(__dirname, 'src'), }, }, });