diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0f5ba90b0..4bd19523d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -34,28 +34,28 @@ jobs: - name: Run tests and linter run: npm run lint && npm test - # Собираем приложение - - name: Build Application - run: npm run build - - # Публикуем приложение на Github Pages - - name: Deploy to Github Pages + # # Собираем приложение + # - name: Build Application + # run: npm run build + + # # Публикуем приложение на Github Pages + # - name: Deploy to Github Pages + # uses: JamesIves/github-pages-deploy-action@4.2.1 + # with: + # branch: gh-pages + # folder: dist + + # Собираем Storybook + - name: Build Storybook + run: npm run build-storybook + + # Публикуем Storybook на Github Pages + - name: Deploy Storybook to Github Pages uses: JamesIves/github-pages-deploy-action@4.2.1 with: branch: gh-pages - folder: dist - - # # Собираем Storybook - # - name: Build Storybook - # run: npm run build-storybook - # - # # Публикуем Storybook на Github Pages - # - name: Deploy Storybook to Github Pages - # uses: JamesIves/github-pages-deploy-action@4.2.1 - # with: - # branch: gh-pages - # folder: storybook-static - # commit-message: "Automatically publish Storybook" + folder: storybook-static + commit-message: "Automatically publish Storybook" # Останавливаем выполнение строго при неудачных тестах - name: Fail on failed tests diff --git a/.storybook/main.ts b/.storybook/main.ts index 3d1c9b2d5..eea5e2c0b 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -1,5 +1,5 @@ const config = { - stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"], + stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)", "../src/**/**/*.stories.@(js|jsx|ts|tsx)"], addons: [ "@storybook/addon-links", "@storybook/addon-essentials", diff --git a/package-lock.json b/package-lock.json index 5b1dfa392..f8dd96c7c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,6 +46,7 @@ "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-storybook": "^0.8.0", "fork-ts-checker-webpack-plugin": "^8.0.0", + "gh-pages": "^6.3.0", "html-webpack-plugin": "^5.5.1", "husky": "^8.0.0", "jest": "^29.5.0", @@ -11021,6 +11022,13 @@ "integrity": "sha512-8KR114CAYQ4/r5EIEsOmOMqQ9j0MRbJZR3aXD/KFA8RuKzyoUB4XrUCg+l8RUGqTVQgKNIgTpjaG8YHRPAbX2w==", "dev": true }, + "node_modules/email-addresses": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/email-addresses/-/email-addresses-5.0.0.tgz", + "integrity": "sha512-4OIPYlA6JXqtVn8zpHpGiI7vE6EQOAg16aGnDMIAlZVinnoZ8208tW1hAbjWydgN/4PLTT9q+O1K6AH/vALJGw==", + "dev": true, + "license": "MIT" + }, "node_modules/emittery": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", @@ -12511,6 +12519,34 @@ "node": ">=10" } }, + "node_modules/filename-reserved-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz", + "integrity": "sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/filenamify": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-4.3.0.tgz", + "integrity": "sha512-hcFKyUG57yWGAzu1CMt/dPzYZuv+jAJUT85bL8mrXvNe6hWj6yEHEc4EdcgiA6Z3oi1/9wXJdZPXF2dZNgwgOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "filename-reserved-regex": "^2.0.0", + "strip-outer": "^1.0.1", + "trim-repeated": "^1.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -13147,6 +13183,39 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gh-pages": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/gh-pages/-/gh-pages-6.3.0.tgz", + "integrity": "sha512-Ot5lU6jK0Eb+sszG8pciXdjMXdBJ5wODvgjR+imihTqsUWF2K6dJ9HST55lgqcs8wWcw6o6wAsUzfcYRhJPXbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "async": "^3.2.4", + "commander": "^13.0.0", + "email-addresses": "^5.0.0", + "filenamify": "^4.3.0", + "find-cache-dir": "^3.3.1", + "fs-extra": "^11.1.1", + "globby": "^11.1.0" + }, + "bin": { + "gh-pages": "bin/gh-pages.js", + "gh-pages-clean": "bin/gh-pages-clean.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/gh-pages/node_modules/commander": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/giget": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/giget/-/giget-1.2.1.tgz", @@ -21335,6 +21404,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strip-outer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz", + "integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/style-loader": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.3.tgz", @@ -21964,6 +22046,19 @@ "node": ">=12" } }, + "node_modules/trim-repeated": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", + "integrity": "sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/trough": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", @@ -31513,6 +31608,12 @@ "integrity": "sha512-8KR114CAYQ4/r5EIEsOmOMqQ9j0MRbJZR3aXD/KFA8RuKzyoUB4XrUCg+l8RUGqTVQgKNIgTpjaG8YHRPAbX2w==", "dev": true }, + "email-addresses": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/email-addresses/-/email-addresses-5.0.0.tgz", + "integrity": "sha512-4OIPYlA6JXqtVn8zpHpGiI7vE6EQOAg16aGnDMIAlZVinnoZ8208tW1hAbjWydgN/4PLTT9q+O1K6AH/vALJGw==", + "dev": true + }, "emittery": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", @@ -32645,6 +32746,23 @@ } } }, + "filename-reserved-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz", + "integrity": "sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ==", + "dev": true + }, + "filenamify": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-4.3.0.tgz", + "integrity": "sha512-hcFKyUG57yWGAzu1CMt/dPzYZuv+jAJUT85bL8mrXvNe6hWj6yEHEc4EdcgiA6Z3oi1/9wXJdZPXF2dZNgwgOg==", + "dev": true, + "requires": { + "filename-reserved-regex": "^2.0.0", + "strip-outer": "^1.0.1", + "trim-repeated": "^1.0.0" + } + }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -33105,6 +33223,29 @@ "get-intrinsic": "^1.1.1" } }, + "gh-pages": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/gh-pages/-/gh-pages-6.3.0.tgz", + "integrity": "sha512-Ot5lU6jK0Eb+sszG8pciXdjMXdBJ5wODvgjR+imihTqsUWF2K6dJ9HST55lgqcs8wWcw6o6wAsUzfcYRhJPXbA==", + "dev": true, + "requires": { + "async": "^3.2.4", + "commander": "^13.0.0", + "email-addresses": "^5.0.0", + "filenamify": "^4.3.0", + "find-cache-dir": "^3.3.1", + "fs-extra": "^11.1.1", + "globby": "^11.1.0" + }, + "dependencies": { + "commander": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "dev": true + } + } + }, "giget": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/giget/-/giget-1.2.1.tgz", @@ -39034,6 +39175,15 @@ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true }, + "strip-outer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz", + "integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.2" + } + }, "style-loader": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.3.tgz", @@ -39512,6 +39662,15 @@ "punycode": "^2.1.1" } }, + "trim-repeated": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", + "integrity": "sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg==", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.2" + } + }, "trough": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", diff --git a/package.json b/package.json index 492664d1f..45db4dba5 100644 --- a/package.json +++ b/package.json @@ -3,10 +3,13 @@ "version": "1.0.0", "description": "Start repo with required configuration", "main": "src/index.tsx", - "author": "Igor ", + "author": "Ivan ", + "homepage": "https://ivandetch.github.io", "scripts": { "start": "webpack serve --mode development", "build": "webpack --mode production", + "predeploy": "npm run build-storybook", + "deploy": "npx gh-pages -d storybook-static", "test": "jest src", "lint": "eslint src --fix", "storybook": "storybook dev -p 6006", @@ -46,6 +49,7 @@ "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-storybook": "^0.8.0", "fork-ts-checker-webpack-plugin": "^8.0.0", + "gh-pages": "^6.3.0", "html-webpack-plugin": "^5.5.1", "husky": "^8.0.0", "jest": "^29.5.0", diff --git a/src/app/App.css b/src/app/App.css index 78b8850cf..d0d0b02eb 100644 --- a/src/app/App.css +++ b/src/app/App.css @@ -28,6 +28,13 @@ color: #61dafb; } +.App-body { + margin: 0 5%; +} +.App-body p { + text-align: justify; +} + @keyframes App-logo-spin { from { transform: rotate(0deg); diff --git a/src/app/App.tsx b/src/app/App.tsx index dcc0ff8ad..257db87f5 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -7,9 +7,35 @@ function App() {
logo -

- Текст писать тут -

+
+

Иван Комраков

+

+ Цель: + Хочу приобрести/структурировать новые знания и навыки о React, чтобы применить их на работе. +

+

+ Технологии: +

    +
  • JavaScript (ES6), jQuery, HTML, CSS3 (препроцессоры SCSS/SASS), Bootstrap, PHP 7.3+, Laravel 8+ .
  • +
  • Частичная работа с React 18, Node.js.
  • +
  • Rest API.
  • +
  • Сборщики: Laravel mix. React vite/WebPack .
  • +
  • Composer, NPM, Kubernets, Docker.
  • +
  • SQL(MySQL,MariaDB), MongoDB.
  • +
  • Git, GitLab, GitLab CI/CD.
  • +
  • CMS WordPress.
  • +
  • Figma, Jira.
  • +
+

+

+ Опыт: + Имею опыт работы в области веб-разработки (профессиональный опыт 7 лет), обладаю знаниями и навыками верстки, Frontend и Backend-технологий. +

+

+ Контакты: + kmrakvi@gmail.com +

+
); diff --git a/src/components/ecommerce/AddToCartButton/AddToCartButton.module.css b/src/components/ecommerce/AddToCartButton/AddToCartButton.module.css new file mode 100644 index 000000000..b7a558c77 --- /dev/null +++ b/src/components/ecommerce/AddToCartButton/AddToCartButton.module.css @@ -0,0 +1,92 @@ + +.addButton { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border: none; + border-radius: 8px; + padding: 12px 24px; + font-size: 14px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.addButton:hover:not(:disabled) { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4); +} + +.addButton:disabled { + opacity: 0.6; + cursor: not-allowed; + transform: none; +} + +.counterWrapper { + display: flex; + align-items: center; + background: white; + border: 2px solid #e9ecef; + border-radius: 8px; + overflow: hidden; +} + +.changeButton { + display: flex; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; + background: #f8f9fa; + border: none; + font-size: 18px; + font-weight: 600; + color: #495057; + cursor: pointer; + transition: all 0.2s ease; +} + +.changeButton:hover:not(:disabled) { + background: #e9ecef; + color: #212529; +} + +.changeButton:disabled { + opacity: 0.4; + cursor: not-allowed; +} + +.counter { + display: flex; + align-items: center; + justify-content: center; + min-width: 40px; + height: 36px; + padding: 0 8px; + font-size: 16px; + font-weight: 600; + color: #212529; + background: white; +} + +/* Адаптивность */ +@media (max-width: 480px) { + .addButton { + padding: 10px 20px; + font-size: 13px; + } + + .changeButton { + width: 32px; + height: 32px; + font-size: 16px; + } + + .counter { + height: 32px; + min-width: 36px; + font-size: 14px; + } +} diff --git a/src/components/ecommerce/AddToCartButton/AddToCartButton.stories.tsx b/src/components/ecommerce/AddToCartButton/AddToCartButton.stories.tsx new file mode 100644 index 000000000..d313c318c --- /dev/null +++ b/src/components/ecommerce/AddToCartButton/AddToCartButton.stories.tsx @@ -0,0 +1,43 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import AddToCartButton from './AddToCartButton'; + +const meta: Meta = { + title: 'Components/E-commerce/AddToCartButton', + component: AddToCartButton, + tags: ['autodocs'], + parameters: { + layout: 'centered', + }, + argTypes: { + count: { + control: { type: 'number', min: 0, max: 10 }, + description: 'Количество товара в корзине', + }, + background: { + control: 'color', + description: 'Изменение цвета фона кнопки', + }, + }, +}; + +export default meta; +type Story = StoryObj; + +export const InitialState: Story = { + args: { + count: 0, + }, + parameters: { + docs: { + description: { + story: 'Начальное состояние - кнопка "В корзину"', + }, + }, + }, +}; + +export const WithCount: Story = { + args: { + count: 3, + }, +}; \ No newline at end of file diff --git a/src/components/ecommerce/AddToCartButton/AddToCartButton.tsx b/src/components/ecommerce/AddToCartButton/AddToCartButton.tsx new file mode 100644 index 000000000..2a7ec0083 --- /dev/null +++ b/src/components/ecommerce/AddToCartButton/AddToCartButton.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import s from './AddToCartButton.module.css'; +import { AddToCartButtonProps } from '../../types'; + +const AddToCartButton: React.FC = ({ count = 0, background }) => { + const handleAdd = () => alert('Товар добавлен в корзину'); + const handleIncrease = () => alert('Увеличить количество'); + const handleDecrease = () => alert('Уменьшить количество'); + if (count === 0) { + return ( + + ); + } + + return ( +
+ +
{count}
+ +
+ ); +}; + +export default AddToCartButton; \ No newline at end of file diff --git a/src/components/ecommerce/AddToCartButton/index.ts b/src/components/ecommerce/AddToCartButton/index.ts new file mode 100644 index 000000000..d9581ec31 --- /dev/null +++ b/src/components/ecommerce/AddToCartButton/index.ts @@ -0,0 +1 @@ +export { default as AddToCartButton } from './AddToCartButton'; \ No newline at end of file diff --git a/src/components/ecommerce/CartItem/CartItem.module.css b/src/components/ecommerce/CartItem/CartItem.module.css new file mode 100644 index 000000000..e86dbece4 --- /dev/null +++ b/src/components/ecommerce/CartItem/CartItem.module.css @@ -0,0 +1,175 @@ +.itemContainer { + background: white; + border: 1px solid #e9ecef; + border-radius: 12px; + padding: 16px; + display: flex; + gap: 16px; + transition: all 0.2s ease; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; +} + +.itemContainer:hover { + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); +} + +.imageContainer { + flex-shrink: 0; + width: 80px; + height: 80px; + border-radius: 8px; + overflow: hidden; + background: #f8f9fa; +} + +.productImage { + width: 100%; + height: 100%; + object-fit: contain; +} + +.details { + flex: 1; + display: flex; + flex-direction: column; + justify-content: space-between; +} + +.headerRow { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 12px; +} + +.title { + font-size: 16px; + font-weight: 600; + color: #2c3e50; + margin: 0; + flex: 1; +} + +.removeBtn { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + background: #f8f9fa; + border: 1px solid #dee2e6; + border-radius: 6px; + cursor: pointer; + flex-shrink: 0; + transition: background 0.2s ease, transform 0.2s ease; +} + +.removeBtn:hover:not(:disabled) { + background: #e74c3c40; + border-color: #e74c3c; + transform: scale(1.1); +} + +.removeBtn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.description { + font-size: 14px; + color: #5d6d7e; + margin: 4px 0 0; + line-height: 1.4; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.footerRow { + display: flex; + justify-content: space-between; + align-items: flex-end; + gap: 16px; + margin-top: auto; +} + +.quantity { + font-size: 14px; + color: #6c757d; +} + +.quantityValue { + font-weight: 600; + color: #495057; +} + +.priceInfo { + text-align: right; +} + +.unitPrice { + color: #5d6d7e; + font-size: 12px; + margin-bottom: 2px; +} + +.totalPrice { + font-size: 18px; + font-weight: 700; + color: #27ae60; +} + +/* Адаптивность */ +@media (max-width: 768px) { + .itemContainer { + padding: 12px; + gap: 12px; + } + + .productImage { + width: 70px; + height: 70px; + } + + .name { + font-size: 15px; + } + + .description { + font-size: 13px; + } + + .footer { + gap: 12px; + } + + .totalPrice { + font-size: 16px; + } +} + +@media (max-width: 480px) { + .itemContainer { + flex-direction: column; + align-items: center; + text-align: center; + } + + .imageContainer { + width: 100%; + height: 120px; + align-self: center; + max-width: 120px; + } + + .footerRow { + flex-direction: column; + gap: 8px; + align-items: center; + } + + .priceInfo { + text-align: center; + } +} \ No newline at end of file diff --git a/src/components/ecommerce/CartItem/CartItem.stories.tsx b/src/components/ecommerce/CartItem/CartItem.stories.tsx new file mode 100644 index 000000000..b452776f3 --- /dev/null +++ b/src/components/ecommerce/CartItem/CartItem.stories.tsx @@ -0,0 +1,45 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import CartItem from './CartItem'; +import { CartItemProps } from '../../types'; + +const meta: Meta = { + title: 'Components/E-commerce/CartItem', + component: CartItem, + tags: ['autodocs'], + parameters: { + layout: 'padded', + }, + argTypes: { + item: { control: 'object' }, + }, +}; + +export default meta; +type Story = StoryObj; + +const exampleItem: CartItemProps['item'] = { + name: 'Larry Carlton X6 Headless 6 Trans Black', + description: 'Headless Electric Guitar with 6 Strings', + price: 65162.53, + image: 'https://bdbo1.thomann.de/thumb/bdb3000/pics/bdbo/19832399.jpg', + category: 'Headless Guitars', + quantity: 1, +}; + +export const SingleItem: Story = { + args: { + item: exampleItem, + }, + parameters: { + docs: { + description: { + story: 'Пример с одним товаром в корзине', + }, + }, + }, +}; +export const MultipleQty: Story = { + args: { + item: { ...exampleItem, quantity: 3 }, + }, +}; \ No newline at end of file diff --git a/src/components/ecommerce/CartItem/CartItem.tsx b/src/components/ecommerce/CartItem/CartItem.tsx new file mode 100644 index 000000000..988d6bc54 --- /dev/null +++ b/src/components/ecommerce/CartItem/CartItem.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import s from './CartItem.module.css'; +import { CartItemProps } from '../../types'; + +const CartItem: React.FC = ({ item }) => { + const { name, description, price, image, quantity } = item; + const totalPrice = price * quantity; + + const handleRemove = () => alert(`Удалить ${name} из корзины`); + + return ( +
+
+ {name} +
+ +
+
+

{name}

+ +
+ +

{description}

+ +
+
+ Количество: {quantity} +
+
+
{price.toLocaleString('ru-RU')} ₽ за шт.
+
{totalPrice.toLocaleString('ru-RU')} ₽
+
+
+
+
+ ); +}; +export default CartItem; \ No newline at end of file diff --git a/src/components/ecommerce/CartItem/index.ts b/src/components/ecommerce/CartItem/index.ts new file mode 100644 index 000000000..cc887d528 --- /dev/null +++ b/src/components/ecommerce/CartItem/index.ts @@ -0,0 +1 @@ +export { default as CartItem } from './CartItem'; \ No newline at end of file diff --git a/src/components/ecommerce/ProductBrief/ProductBrief.module.css b/src/components/ecommerce/ProductBrief/ProductBrief.module.css new file mode 100644 index 000000000..6cef96a76 --- /dev/null +++ b/src/components/ecommerce/ProductBrief/ProductBrief.module.css @@ -0,0 +1,117 @@ +.productBrief { + background: white; + border-radius: 12px; + overflow: hidden; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + border: 1px solid #f0f0f0; + transition: all 0.3s ease; + display: flex; + flex-direction: column; + height: 100%; +} + +.productBrief:hover { + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + transform: translateY(-4px); +} + +.imageContainer { + position: relative; + width: 100%; + height: 200px; + overflow: hidden; + background: #f8f9fa; +} + +.productImage { + width: 100%; + height: 100%; + object-fit: contain; + transition: transform 0.3s ease; +} + +.productBrief:hover .productImage { + transform: scale(1.05); +} + +.details { + padding: 16px; + display: flex; + flex-direction: column; + flex: 1; +} + +.title { + margin: 0 0 8px 0; + font-size: 16px; + font-weight: 600; + color: #2c3e50; + line-height: 1.3; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.description { + margin: 0 0 16px 0; + color: #5d6d7e; + font-size: 14px; + line-height: 1.4; + flex: 1; +} + +.footerRow { + display: flex; + justify-content: space-between; + align-items: center; + gap: 12px; + margin-top: auto; +} + +.priceInfo { + font-size: 18px; + font-weight: 700; + color: #27ae60; +} +.unitPrice { + color: #5d6d7e; + font-size: 16px; + margin-bottom: 2px; +} + +/* Адаптивность */ +@media (max-width: 768px) { + .imageContainer { + height: 180px; + } + + .content { + padding: 14px; + } + + .name { + font-size: 15px; + } + + .description { + font-size: 13px; + } + + .price { + font-size: 16px; + } +} + +@media (max-width: 480px) { + .footer { + flex-direction: column; + gap: 8px; + align-items: stretch; + } + + .price { + text-align: center; + } +} \ No newline at end of file diff --git a/src/components/ecommerce/ProductBrief/ProductBrief.stories.tsx b/src/components/ecommerce/ProductBrief/ProductBrief.stories.tsx new file mode 100644 index 000000000..22958885a --- /dev/null +++ b/src/components/ecommerce/ProductBrief/ProductBrief.stories.tsx @@ -0,0 +1,43 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import ProductBrief from './ProductBrief'; +import { ProductBriefProps } from '../../types'; + +const meta: Meta = { + title: 'Components/E-commerce/ProductBrief', + component: ProductBrief, + tags: ['autodocs'], + parameters: { + layout: 'padded', + }, + argTypes: { + product: { control: 'object' }, + maxDescriptionLength: { + control: { type: 'number', min: 20, max: 200, step: 10 }, + description: 'Максимальная длина описания', + }, + }, +}; + +export default meta; +type Story = StoryObj; + +const exampProductBrief: ProductBriefProps['product'] = { + name: 'Larry Carlton X6 Headless 6 Trans Black', + description: 'Headless Electric Guitar with 6 Strings', + price: 65162.53, + image: 'https://bdbo1.thomann.de/thumb/bdb3000/pics/bdbo/19832399.jpg', + category: 'Headless Guitars', +}; + +export const Default: Story = { + args: { + product: exampProductBrief, + maxDescriptionLength: 80, + }, +}; +export const LongText: Story = { + args: { + product: exampProductBrief, + maxDescriptionLength: 30, + }, +}; \ No newline at end of file diff --git a/src/components/ecommerce/ProductBrief/ProductBrief.tsx b/src/components/ecommerce/ProductBrief/ProductBrief.tsx new file mode 100644 index 000000000..1513d559a --- /dev/null +++ b/src/components/ecommerce/ProductBrief/ProductBrief.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import AddToCartButton from '../AddToCartButton/AddToCartButton'; +import { ProductBriefProps } from '../../types'; +import s from './ProductBrief.module.css'; + +const ProductBrief: React.FC = ({ product, maxDescriptionLength = 80 }) => { + const { name, description, price, image } = product; + + const truncatedDesc = + description.length > maxDescriptionLength + ? description.slice(0, maxDescriptionLength) + '...' + : description; + + return ( +
+
+ {name} +
+ +
+

{name}

+

+ {truncatedDesc} +

+ +
+
{price.toLocaleString('ru-RU')} ₽
+ +
+
+
+ ); +}; +export default ProductBrief; \ No newline at end of file diff --git a/src/components/ecommerce/ProductBrief/index.ts b/src/components/ecommerce/ProductBrief/index.ts new file mode 100644 index 000000000..c8e3a309c --- /dev/null +++ b/src/components/ecommerce/ProductBrief/index.ts @@ -0,0 +1 @@ +export { default as ProductBrief } from './ProductBrief'; \ No newline at end of file diff --git a/src/components/ecommerce/ProductFull/ProductFull.module.css b/src/components/ecommerce/ProductFull/ProductFull.module.css new file mode 100644 index 000000000..61f8fff7c --- /dev/null +++ b/src/components/ecommerce/ProductFull/ProductFull.module.css @@ -0,0 +1,162 @@ +.productFull { + background: white; + border-radius: 16px; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1); + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + overflow: hidden; + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0; + max-width: 1000px; + margin: 0 auto; +} + +.imageSection { + background: #ffffff; + display: flex; + align-items: center; + justify-content: center; + padding: 32px; +} + +.imageContainer { + width: 100%; + max-width: 400px; + aspect-ratio: 1; + border-radius: 12px; + overflow: hidden; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5); +} + +.productImage { + width: 100%; + height: 100%; + object-fit: contain; + transition: transform 0.3s ease; +} + +.productImage:hover { + transform: scale(1.05); +} + +.details { + padding: 32px; + display: flex; + flex-direction: column; + justify-content: space-between; +} + +.headerRow { + margin-bottom: 20px; +} + +.category { + display: inline-block; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + padding: 6px 12px; + border-radius: 8px; + font-size: 12px; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.5px; + margin-bottom: 12px; +} + +.title { + margin: 0 0 16px 0; + font-size: 28px; + font-weight: 700; + color: #2c3e50; +} + +.unitPrice { + font-size: 26px; + font-weight: 800; + color: #000000; + margin-bottom: 8px; +} + +.description { + margin-bottom: 32px; + flex: 1; +} + +.descriptionTitle { + margin: 0 0 12px 0; + font-size: 18px; + font-weight: 600; + color: #34495e; +} + +.descriptionText { + margin: 0; + color: #5d6d7e; + font-size: 16px; + line-height: 1.6; +} +.bottomRow{ + display:flex; + justify-content:space-between; + align-items:center +} + +.actions { + display: flex; + gap: 16px; + align-items: center; +} + +/* Адаптивность */ +@media (max-width: 768px) { + .productFull { + grid-template-columns: 1fr; + max-width: 600px; + } + + .imageSection { + padding: 24px; + order: 1; + } + + .contentSection { + padding: 24px; + order: 2; + } + + .title { + font-size: 24px; + } + + .unitPrice { + font-size: 28px; + } + + .descriptionTitle { + font-size: 16px; + } + + .descriptionText { + font-size: 15px; + } +} + +@media (max-width: 480px) { + .imageSection, + .contentSection { + padding: 16px; + } + + .title { + font-size: 20px; + } + + .unitPrice { + font-size: 24px; + } + + .categoryBadge { + font-size: 11px; + padding: 4px 8px; + } +} \ No newline at end of file diff --git a/src/components/ecommerce/ProductFull/ProductFull.stories.tsx b/src/components/ecommerce/ProductFull/ProductFull.stories.tsx new file mode 100644 index 000000000..261500208 --- /dev/null +++ b/src/components/ecommerce/ProductFull/ProductFull.stories.tsx @@ -0,0 +1,36 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import ProductFull from './ProductFull'; +import { ProductFullProps } from '../../types'; + +const meta: Meta = { + title: 'Components/E-commerce/ProductFull', + component: ProductFull, + tags: ['autodocs'], + parameters: { + layout: 'fullscreen', + }, +}; + +export default meta; +type Story = StoryObj; + +const exampleProductFull: ProductFullProps['product'] = { + name: 'Larry Carlton X6 Headless 6 Trans Black', + description: 'Stirring into the innovative world of instruments, Sire produces its own headless guitars. The new X6 offers a combination of modern sleek design and exceptional functionality, constructed with a lightweight build (approximately 6–7 lbs.) to ensure playing at ease. The X6 delivers a powerful and dynamic sound, making it a perfect choice for any players who want to go trendy without compromising performance. Available in 6 and 7-string variants.', + price: 65162.53, + image: 'https://bdbo1.thomann.de/thumb/bdb3000/pics/bdbo/19832399.jpg', + category: 'Headless Guitars', +}; + +export const Default: Story = { + args: { + product: exampleProductFull, + }, + parameters: { + docs: { + description: { + story: 'Детальная страница смартфона с полным описанием', + }, + }, + }, +}; \ No newline at end of file diff --git a/src/components/ecommerce/ProductFull/ProductFull.tsx b/src/components/ecommerce/ProductFull/ProductFull.tsx new file mode 100644 index 000000000..f3dbadcab --- /dev/null +++ b/src/components/ecommerce/ProductFull/ProductFull.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import AddToCartButton from '../AddToCartButton/AddToCartButton'; +import { ProductFullProps } from '../../types'; +import s from './ProductFull.module.css'; + +const ProductFull: React.FC = ({ product }) => { + const { name, description, price, image, category } = product; + + return ( +
+
+
+ {name} +
+
+ +
+
+
{category}
+

{name}

+
+ +
+

Описание:

+

{description}

+
+ +
+
{price.toLocaleString('ru-RU')} ₽
+
+ +
+
+
+
+ ); +}; +export default ProductFull; \ No newline at end of file diff --git a/src/components/ecommerce/ProductFull/index.ts b/src/components/ecommerce/ProductFull/index.ts new file mode 100644 index 000000000..1aa8537a7 --- /dev/null +++ b/src/components/ecommerce/ProductFull/index.ts @@ -0,0 +1 @@ +export { default as ProductFull } from './ProductFull'; \ No newline at end of file diff --git a/src/components/ecommerce/index.ts b/src/components/ecommerce/index.ts new file mode 100644 index 000000000..e0522cb84 --- /dev/null +++ b/src/components/ecommerce/index.ts @@ -0,0 +1,4 @@ +export { AddToCartButton } from './AddToCartButton'; +export { CartItem } from './CartItem'; +export { ProductBrief } from './ProductBrief'; +export { ProductFull } from './ProductFull'; \ No newline at end of file diff --git a/src/components/finance/Transaction/TransactionBrief/TransactionBrief.module.css b/src/components/finance/Transaction/TransactionBrief/TransactionBrief.module.css new file mode 100644 index 000000000..da5160e5b --- /dev/null +++ b/src/components/finance/Transaction/TransactionBrief/TransactionBrief.module.css @@ -0,0 +1,91 @@ +.cardBrief { + background: #ffffff; + border-radius: 12px; + padding: 16px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); + border: 1px solid #f0f0f0; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + transition: all 0.2s ease; +} + +.cardBrief:hover { + box-shadow: 0 4px 12rem rgba(0, 0, 0, 0.1); + transform: translateY(-2px); +} + +.header { + display: flex; + justify-content: space-between; + align-items: start; + margin-bottom: 8px; +} + +.titleGroup { + display: flex; + flex-direction: column; + gap: 8px; +} + +.title { + margin: 0; + font-size: 18px; + font-weight: 600; + color: #000000; + line-height: 100%; +} + +.category { + display: inline-block; + background: linear-gradient(135deg, #4b6cb7, #182848); + color: #ffffff; + padding: 4px 8px; + border-radius: 10px; + font-size: 12px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + width: fit-content; +} + +.amount { + font-size: 16px; + font-weight: bold; + white-space: nowrap; +} + +.income { + color: #2fb307; +} + +.expense { + color: #f52c16; +} + +.description { + margin: 10px 0 0 0; + font-size: 14px; + color: #333; + line-height: 1.4; +} + +/* Адаптивность */ +@media (max-width: 768px) { + .header { + flex-direction: column; + margin-bottom: 10px; + } + + .amount { + font-size: 15px; + } + + .title { + font-size: 16px; + } +} + +@media (max-width: 480px) { + .cardBrief { + padding: 12px; + } +} \ No newline at end of file diff --git a/src/components/finance/Transaction/TransactionBrief/TransactionBrief.stories.tsx b/src/components/finance/Transaction/TransactionBrief/TransactionBrief.stories.tsx new file mode 100644 index 000000000..6bc36fac8 --- /dev/null +++ b/src/components/finance/Transaction/TransactionBrief/TransactionBrief.stories.tsx @@ -0,0 +1,56 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import TransactionBrief from './TransactionBrief'; + +const meta: Meta = { + title: 'Components/Finance/Transaction/TransactionBrief', + component: TransactionBrief, + tags: ['autodocs'], + parameters: { + layout: 'padded', + }, + argTypes: { + amount: { + control: { type: 'number' }, + description: 'Сумма операции', + }, + category: { + control: 'text', + description: 'Категория', + }, + title: { + control: 'text', + description: 'Название операции', + }, + description: { + control: 'text', + description: 'Описание операции', + }, + maxDescriptionLength: { + control: { type: 'number', min: 10, max: 200, step: 10 }, + description: 'Максимальная длина описания', + }, + }, +}; + +export default meta; +type Story = StoryObj; + +export const IncomeTransaction: Story = { + args: { + amount: 200000, + category: 'Зарплата', + title: 'Перевод от работодателя', + description: 'Получена зарплата за август.', + maxDescriptionLength: 60, + }, +}; + +export const ExpenseTransaction: Story = { + args: { + amount: -2750, + category: 'Еда', + title: 'Покупка в магазине', + description: 'Купил продукты в супермаркете на неделю: овощи, фрукты, мясо, хлеб и молочные продукты.', + maxDescriptionLength: 60, + }, +}; \ No newline at end of file diff --git a/src/components/finance/Transaction/TransactionBrief/TransactionBrief.tsx b/src/components/finance/Transaction/TransactionBrief/TransactionBrief.tsx new file mode 100644 index 000000000..1f3b813fb --- /dev/null +++ b/src/components/finance/Transaction/TransactionBrief/TransactionBrief.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import s from './TransactionBrief.module.css'; +import { TransactionBrief as TransactionBriefProps } from '../../../types'; + +const TransactionBrief: React.FC = ({ + amount, + category, + title, + description, + maxDescriptionLength = 60, +}) => { + const isPositive = amount > 0; + const sign = isPositive ? '+' : '-'; + const formattedAmount = `${sign}${Math.abs(amount).toLocaleString('ru-RU')} ₽`; + const truncatedDesc = description.length > maxDescriptionLength + ? description.slice(0, maxDescriptionLength) + '...' + : description; + + return ( +
+
+
+

{title}

+ {category} +
+
+ {formattedAmount} +
+
+ {description &&

{truncatedDesc}

} +
+ ); +}; +export default TransactionBrief; \ No newline at end of file diff --git a/src/components/finance/Transaction/TransactionBrief/index.ts b/src/components/finance/Transaction/TransactionBrief/index.ts new file mode 100644 index 000000000..18c87fe25 --- /dev/null +++ b/src/components/finance/Transaction/TransactionBrief/index.ts @@ -0,0 +1 @@ +export { default as TransactionBrief } from './TransactionBrief'; \ No newline at end of file diff --git a/src/components/finance/Transaction/TransactionFull/TransactionFull.module.css b/src/components/finance/Transaction/TransactionFull/TransactionFull.module.css new file mode 100644 index 000000000..dfa819fcc --- /dev/null +++ b/src/components/finance/Transaction/TransactionFull/TransactionFull.module.css @@ -0,0 +1,166 @@ +.cardFull { + background: #ffffff; + border-radius: 12px; + padding: 20px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); + border: 1px solid #f0f0f0; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + transition: all 0.2s ease; +} + +.cardFull:hover { + box-shadow: 0 4px 12rem rgba(0, 0, 0, 0.1); + transform: translateY(-2px); +} + +.header { + display: flex; + justify-content: space-between; + align-items: start; + margin-bottom: 8px; + flex-wrap: wrap; +} + +.titleGroup { + display: flex; + flex-direction: column; + gap: 8px; + flex-grow: 1; +} + +.title { + margin: 0; + font-size: 18px; + font-weight: 600; + color: #000000; + line-height: 100%; +} + +.meta { + display: flex; + gap: 12px; + align-items: center; + margin-top: 4px; + font-size: 13px; + color: #6b6b6b; +} + +.category { + display: inline-block; + background: linear-gradient(135deg, #4b6cb7, #182848); + color: #ffffff; + padding: 4px 8px; + border-radius: 10px; + font-size: 12px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + width: fit-content; +} + +.date { + color: #7f8c8d; + font-size: 14px; + font-weight: 500; +} + +.actions { + display: flex; + align-items: center; + gap: 12px; +} + +.amount { + font-size: 16px; + font-weight: bold; + white-space: nowrap; +} + +.income { + color: #2fb307; +} + +.expense { + color: #f52c16; +} + +.editButton { + display: flex; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; + background: #f8f9fa; + border: 1px solid #dee2e6; + border-radius: 8px; + font-size: 18px; + cursor: pointer; + transition: all 0.2s ease; + flex-shrink: 0; +} + +.editButton:hover:not(:disabled) { + background: #e9ecef; + border-color: #adb5bd; + transform: scale(1.05); +} + +.editButton:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.descriptionBlock { + margin-top: 16px; + border-top: 1px solid #ecf0f1; +} + +.descriptionTitle { + font-size: 15px; + font-weight: 500; + margin-bottom: 4px; + color: #34495e; +} + +.descriptionText { + margin: 0; + color: #5d6d7e; + font-size: 14px; + line-height: 1.5; +} + +/* Адаптивность */ +@media (max-width: 768px) { + .header { + flex-direction: column; + gap: 12px; + } + + .amount { + font-size: 16px; + } + + .title { + font-size: 18px; + } + + .meta { + gap: 8px; + } +} + +@media (max-width: 480px) { + .cardFull { + padding: 12px; + } + + .actions { + gap: 8px; + } + + .editButton { + width: 32px; + height: 32px; + font-size: 14px; + } +} \ No newline at end of file diff --git a/src/components/finance/Transaction/TransactionFull/TransactionFull.stories.tsx b/src/components/finance/Transaction/TransactionFull/TransactionFull.stories.tsx new file mode 100644 index 000000000..bb527cc64 --- /dev/null +++ b/src/components/finance/Transaction/TransactionFull/TransactionFull.stories.tsx @@ -0,0 +1,47 @@ +import TransactionFull from './TransactionFull'; +import type { Meta, StoryObj } from '@storybook/react'; + +const meta: Meta = { + title: 'Components/Finance/Transaction/TransactionFull', + component: TransactionFull, + tags: ['autodocs'], + parameters: { + layout: 'padded', + }, + argTypes: { + amount: { control: 'number', description: 'Сумма операции' }, + category: { control: 'text', description: 'Категория' }, + title: { control: 'text', description: 'Название операции' }, + description: { control: 'text', description: 'Описание операции' }, + date: { control: 'date', description: 'Дата операции' }, + maxDescriptionLength: { + control: { type: 'number', min: 10, max: 200, step: 10 }, + description: 'Максимальная длина описания', + }, + }, +}; + +export default meta; +type Story = StoryObj; + +export const IncomeTransaction: Story = { + args: { + amount: 200000, + category: 'Зарплата', + title: 'Перевод от работодателя', + description: 'Зарплата за август 2025 года, включая премию за успешный проект.', + date: new Date('2025-08-01').toISOString(), + maxDescriptionLength: 60, + }, +}; + +export const ExpenseTransaction: Story = { + args: { + amount: -4600, + category: 'Коммунальные услуги', + title: 'Оплата ЖКХ', + description: 'Платёж за июль: электричество, вода, газ и вывоз мусора.', + date: new Date('2025-07-31').toISOString(), + maxDescriptionLength: 60, + }, +}; \ No newline at end of file diff --git a/src/components/finance/Transaction/TransactionFull/TransactionFull.tsx b/src/components/finance/Transaction/TransactionFull/TransactionFull.tsx new file mode 100644 index 000000000..d350ad5a1 --- /dev/null +++ b/src/components/finance/Transaction/TransactionFull/TransactionFull.tsx @@ -0,0 +1,58 @@ +/* eslint-disable prettier/prettier */ +import React from 'react'; +import s from './TransactionFull.module.css'; +import { TransactionFull as TransactionFullProps } from '../../../types'; + +const TransactionFull: React.FC = ({ + amount, + category, + title, + description, + date, + maxDescriptionLength = 60, +}) => { + const isIncome = amount > 0; + const sign = isIncome ? '+' : '-'; + const formattedAmount = `${sign}${Math.abs(amount).toLocaleString('ru-RU')} ₽`; + const formattedDate = new Date(date).toLocaleDateString('ru-RU', { + day: '2-digit', + month: 'long', + year: 'numeric', + }); + const truncatedDesc = description.length > maxDescriptionLength + ? description.slice(0, maxDescriptionLength) + '...' + : description; + + return ( +
+
+
+

{title}

+
+ {category} + {formattedDate} +
+
+
+
+ {formattedAmount} +
+ +
+
+ {description && ( +
+

Описание:

+

{truncatedDesc}

+
+ )} +
+ ); +}; +export default TransactionFull; \ No newline at end of file diff --git a/src/components/finance/Transaction/TransactionFull/index.ts b/src/components/finance/Transaction/TransactionFull/index.ts new file mode 100644 index 000000000..5849077d5 --- /dev/null +++ b/src/components/finance/Transaction/TransactionFull/index.ts @@ -0,0 +1 @@ +export { default as TransactionFull } from './TransactionFull'; \ No newline at end of file diff --git a/src/components/finance/Transaction/index.ts b/src/components/finance/Transaction/index.ts new file mode 100644 index 000000000..6e6f019ab --- /dev/null +++ b/src/components/finance/Transaction/index.ts @@ -0,0 +1,3 @@ +/* eslint-disable prettier/prettier */ +export { TransactionBrief } from './TransactionBrief'; +export { TransactionFull } from './TransactionFull'; \ No newline at end of file diff --git a/src/components/finance/index.ts b/src/components/finance/index.ts new file mode 100644 index 000000000..b3f624dfb --- /dev/null +++ b/src/components/finance/index.ts @@ -0,0 +1,2 @@ +export { TransactionBrief } from './Transaction/TransactionBrief'; +export { TransactionFull } from './Transaction/TransactionFull'; \ No newline at end of file diff --git a/src/components/index.ts b/src/components/index.ts new file mode 100644 index 000000000..fa3183bd7 --- /dev/null +++ b/src/components/index.ts @@ -0,0 +1,21 @@ +/* eslint-disable prettier/prettier */ +// General Components +export { Header } from './layout/Header'; +export { Layout } from './layout/Layout'; + +// UI Components +export { Logo } from './ui/Logo'; +export { Modal } from './ui/Modal'; + +// E-commerce Components +export { AddToCartButton } from './ecommerce/AddToCartButton'; +export { CartItem } from './ecommerce/CartItem'; +export { ProductBrief } from './ecommerce/ProductBrief'; +export { ProductFull } from './ecommerce/ProductFull'; + +// Transaction Components +export { TransactionBrief } from './finance/Transaction/TransactionBrief'; +export { TransactionFull } from './finance/Transaction/TransactionFull'; + +// Types +export * from './types'; \ No newline at end of file diff --git a/src/components/layout/Header/Header.module.css b/src/components/layout/Header/Header.module.css new file mode 100644 index 000000000..3ec5eb6d0 --- /dev/null +++ b/src/components/layout/Header/Header.module.css @@ -0,0 +1,29 @@ +.headerWrapper { + width: 100%; + position: sticky; + top: 0; + background-color: #ffffff; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05); + z-index: 1000; +} + +.headerContainer { + max-width: 1200px; + margin: 0 auto; + padding: 1rem 2rem; + display: flex; + align-items: center; + justify-content: space-between; +} + +.logoBlock { + display: flex; + align-items: center; +} + +.navContent { + display: flex; + gap: 1.5rem; + font-weight: 500; + color: #333; +} \ No newline at end of file diff --git a/src/components/layout/Header/Header.stories.tsx b/src/components/layout/Header/Header.stories.tsx new file mode 100644 index 000000000..ddde81835 --- /dev/null +++ b/src/components/layout/Header/Header.stories.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; +import Header from './Header'; + +const meta: Meta = { + title: 'Components/Layout/Header', + component: Header, + tags: ['autodocs'], + parameters: { + layout: 'fullscreen', + docs: { + description: { + component: 'Заголовок с логотипом и кастомным контентом внутри. Поддерживает HTML для навигации.', + }, + }, + }, + argTypes: { + children: { + control: 'text', + name: 'Children HTML', + description: 'Дополнительный контент в правой части хедера', + }, + className: { + control: 'text', + description: 'Дополнительные CSS классы', + }, + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + className: '', + }, +}; + +export const Playground: Story = { + args: { + className: '', + children: ``, + }, + render: (args) => { + const { className, children } = args; + // Convert children to HTML if it's a string + const html = typeof children === 'string' ? children : ''; + return ( +
+
+
+ ); + }, +}; \ No newline at end of file diff --git a/src/components/layout/Header/Header.tsx b/src/components/layout/Header/Header.tsx new file mode 100644 index 000000000..758c72ebf --- /dev/null +++ b/src/components/layout/Header/Header.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import Logo from '../../ui/Logo/Logo'; +import { HeaderProps } from '../../types'; +import s from './Header.module.css'; + + +const Header: React.FC = ({ children, className }) => { + return ( +
+
+
+ +
+ {children &&
{children}
} +
+
+ ); +}; + +export default Header; \ No newline at end of file diff --git a/src/components/layout/Header/index.ts b/src/components/layout/Header/index.ts new file mode 100644 index 000000000..2d319850d --- /dev/null +++ b/src/components/layout/Header/index.ts @@ -0,0 +1 @@ +export { default as Header } from './Header'; \ No newline at end of file diff --git a/src/components/layout/Layout/Layout.module.css b/src/components/layout/Layout/Layout.module.css new file mode 100644 index 000000000..4ac80a339 --- /dev/null +++ b/src/components/layout/Layout/Layout.module.css @@ -0,0 +1,28 @@ +.layoutWrapper { + display: flex; + flex-direction: column; + min-height: 100vh; + background-color: #f8f9fa; +} + +.mainContent { + flex: 1; + padding: 2rem; + max-width: 1200px; + margin: 0 auto; + width: 100%; + box-sizing: border-box; +} + +/* Адаптивность */ +@media (max-width: 768px) { + .mainContent { + padding: 1rem; + } +} + +@media (max-width: 480px) { + .mainContent { + padding: 0.8rem; + } +} \ No newline at end of file diff --git a/src/components/layout/Layout/Layout.stories.tsx b/src/components/layout/Layout/Layout.stories.tsx new file mode 100644 index 000000000..5a5c4876f --- /dev/null +++ b/src/components/layout/Layout/Layout.stories.tsx @@ -0,0 +1,65 @@ +import React from 'react'; +import Layout from './Layout'; +import type { Meta, StoryObj } from '@storybook/react'; + +const meta: Meta = { + title: 'Components/Layout/Layout', + component: Layout, + tags: ['autodocs'], + parameters: { + layout: 'fullscreen', + }, + argTypes: { + children: { + control: 'text', + description: 'Основной контент, отображаемый под шапкой', + }, + }, +}; + +export default meta; +type Story = StoryObj; + +const defaultChildren = ( +
+

Добро пожаловать в интерфейс OTUS

+

Этот макет включает прилипший Header с логотипом и областью контента ниже.

+

Вы можете использовать этот Layout для страниц дашбордов, профиля, каталога и др.

+
+); + +const longChildren = ( +
+

Прокрутка и фиксированный Header

+

Ниже находятся секции, каждая с уникальным содержимым.

+ {Array.from({ length: 5 }, (_, i) => ( +
+

Секция {i + 1}

+

+ Это демонстрация длинного контента. Прокрутите вниз и убедитесь, что Header остаётся на месте. +

+
+ ))} +
+); + +export const Default: Story = { + args: { + children: defaultChildren, + }, +}; + +export const LongContent: Story = { + args: { + children: longChildren, + }, +}; \ No newline at end of file diff --git a/src/components/layout/Layout/Layout.tsx b/src/components/layout/Layout/Layout.tsx new file mode 100644 index 000000000..c74179b65 --- /dev/null +++ b/src/components/layout/Layout/Layout.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { LayoutProps } from '../../types'; +import { Header } from '../Header'; +import s from './Layout.module.css'; + +const Layout: React.FC = ({ children, className }) => { + return ( +
+
+
+ {children} +
+
+ ); +}; + +export default Layout; \ No newline at end of file diff --git a/src/components/layout/Layout/index.ts b/src/components/layout/Layout/index.ts new file mode 100644 index 000000000..65fbebe83 --- /dev/null +++ b/src/components/layout/Layout/index.ts @@ -0,0 +1 @@ +export { default as Layout } from './Layout'; \ No newline at end of file diff --git a/src/components/layout/index.ts b/src/components/layout/index.ts new file mode 100644 index 000000000..121ddb379 --- /dev/null +++ b/src/components/layout/index.ts @@ -0,0 +1,2 @@ +export { Header } from './Header'; +export { Layout } from './Layout'; \ No newline at end of file diff --git a/src/components/types.ts b/src/components/types.ts new file mode 100644 index 000000000..ffabee68a --- /dev/null +++ b/src/components/types.ts @@ -0,0 +1,76 @@ +import { ReactNode } from 'react'; + +// General component interfaces +export interface HeaderProps { + children?: ReactNode; + className?: string; +} +export interface LayoutProps { + children: ReactNode; + className?: string; +} + +// UI component interfaces +export enum Sizes { + small = 'small', + medium = 'medium', + large = 'large', +} + +export interface LogoProps { + size?: Sizes; + children?: React.ReactNode; +} + +export interface ModalProps { + visible: boolean; + children?: React.ReactNode; + onClose: () => void; +} + +// Transaction component +export interface Transaction { + amount: number; + category: string; + title: string; + description: string; +} + +export interface TransactionBrief extends Transaction { + maxDescriptionLength?: number; +} + +export interface TransactionFull extends TransactionBrief { + date: string; +} + +// Ecommerce component +export interface Product { + name: string; + description: string; + price: number; + image: string; + category: string; +} + +export interface CartItem extends Product { + quantity: number; +} + +export interface AddToCartButtonProps { + count: number; + background?: string; +} + +export interface ProductBriefProps { + product: Product; + maxDescriptionLength?: number; +} + +export interface ProductFullProps { + product: Product; +} + +export interface CartItemProps { + item: CartItem; +} \ No newline at end of file diff --git a/src/components/ui/Logo/Logo.module.css b/src/components/ui/Logo/Logo.module.css new file mode 100644 index 000000000..0fb69c6a4 --- /dev/null +++ b/src/components/ui/Logo/Logo.module.css @@ -0,0 +1,52 @@ +.logoContainer { + display: flex; + align-items: center; + gap: 8px; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + user-select: none; +} + +.logoCircle { + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border-radius: 50%; + background: linear-gradient(135deg, #4b6cb7, #182848); +} + +.logoText { + font-weight: 600; + font-size: 1.5rem; + color: #333; +} + +/* Размеры */ +.small .logoCircle { + width: 20px; + height: 20px; +} + +.small .logoText { + font-size: 1rem; +} + +.medium .logoCircle { + width: 28px; + height: 28px; +} + +.medium .logoText { + font-size: 1.25rem; +} + +.large .logoCircle { + width: 36px; + height: 36px; +} + +.large .logoText { + font-size: 1.75rem; +} \ No newline at end of file diff --git a/src/components/ui/Logo/Logo.stories.tsx b/src/components/ui/Logo/Logo.stories.tsx new file mode 100644 index 000000000..231c922e0 --- /dev/null +++ b/src/components/ui/Logo/Logo.stories.tsx @@ -0,0 +1,44 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import Logo from './Logo'; +import { Sizes } from '../../types'; + +const meta: Meta = { + title: 'Components/UI/Logo', + component: Logo, + tags: ['autodocs'], + parameters: { + layout: 'centered', + }, + argTypes: { + size: { + control: { type: 'select' }, + options: ['small', 'medium', 'large'], + description: 'Размер логотипа', + }, + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + size: Sizes.medium, + }, +}; + +export const Small: Story = { + args: { + size: Sizes.small, + }, +}; +export const Medium: Story = { + args: { + size: Sizes.medium, + }, +}; +export const Large: Story = { + args: { + size: Sizes.large, + }, +}; \ No newline at end of file diff --git a/src/components/ui/Logo/Logo.tsx b/src/components/ui/Logo/Logo.tsx new file mode 100644 index 000000000..5ac8a4edd --- /dev/null +++ b/src/components/ui/Logo/Logo.tsx @@ -0,0 +1,14 @@ +import React, { FC } from 'react'; +import s from './Logo.module.css'; +import { LogoProps, Sizes } from '../../types'; + +const Logo: React.FC = ({ size = Sizes.medium }) => { + return ( +
+
+ MyBrand +
+ ); +}; + +export default Logo; diff --git a/src/components/ui/Logo/index.ts b/src/components/ui/Logo/index.ts new file mode 100644 index 000000000..7b6a8b1bf --- /dev/null +++ b/src/components/ui/Logo/index.ts @@ -0,0 +1 @@ +export { default as Logo } from './Logo'; \ No newline at end of file diff --git a/src/components/ui/Modal/Modal.module.css b/src/components/ui/Modal/Modal.module.css new file mode 100644 index 000000000..2ebd5da89 --- /dev/null +++ b/src/components/ui/Modal/Modal.module.css @@ -0,0 +1,55 @@ +/* Modal.module.css */ + +.backdrop { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background: rgba(0, 0, 0, 0.6); + display: flex; + justify-content: center; + align-items: center; + z-index: 9999; +} + +.container { + background-color: #ffffff; + padding: 2rem; + border-radius: 16px; + max-width: 500px; + width: 90%; + box-shadow: 0 8px 30px rgba(0, 0, 0, 0.2); + position: relative; +} + +.dismiss { + position: absolute; + top: 12px; + right: 12px; + background: none; + border: none; + font-size: 1.5rem; + cursor: pointer; + color: #333; + padding: 0; + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 4px; + transition: background-color 0.2s, color 0.2s; +} + +.closeButton:hover { + background-color: #f5f5f5; + color: #333; +} + +.content { + margin-top: 1.5rem; + color: #333; + font-size: 1rem; + line-height: 1.6; +} \ No newline at end of file diff --git a/src/components/ui/Modal/Modal.stories.tsx b/src/components/ui/Modal/Modal.stories.tsx new file mode 100644 index 000000000..92bb98435 --- /dev/null +++ b/src/components/ui/Modal/Modal.stories.tsx @@ -0,0 +1,47 @@ +// Modal.stories.tsx +import React, { useState } from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; +import Modal from './Modal'; + +const meta: Meta = { + title: 'Components/UI/Modal', + component: Modal, + tags: ['autodocs'], + parameters: { + layout: 'fullscreen', + }, + argTypes: { + visible: { + control: 'boolean', + description: 'Управляет видимостью модального окна', + }, + onClose: { action: 'closed' }, + children: { control: 'text' }, + }, +}; + +export default meta; +type Story = StoryObj; + +export const Basic: Story = { + args: { + visible: true, + children: 'Пример содержимого модального окна' + }, +}; + +export const WithToggle: Story = { + render: (args) => { + const [open, setOpen] = useState(true); + + return ( + <> + + setOpen(false)} /> + + ); + }, + args: { + children: 'Это модальное окно можно закрыть нажатием на крестик' + } +}; \ No newline at end of file diff --git a/src/components/ui/Modal/Modal.tsx b/src/components/ui/Modal/Modal.tsx new file mode 100644 index 000000000..614fa5a44 --- /dev/null +++ b/src/components/ui/Modal/Modal.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import s from './Modal.module.css'; +import { ModalProps } from '../../types'; + +const Modal: React.FC = ({ visible, children, onClose }) => { + if (!visible) return null; + + return ( +
+
+ +
{children}
+
+
+ ); +}; +export default Modal; \ No newline at end of file diff --git a/src/components/ui/Modal/index.ts b/src/components/ui/Modal/index.ts new file mode 100644 index 000000000..48ca212d7 --- /dev/null +++ b/src/components/ui/Modal/index.ts @@ -0,0 +1 @@ +export { default as Modal } from './Modal'; \ No newline at end of file diff --git a/src/components/ui/index.ts b/src/components/ui/index.ts new file mode 100644 index 000000000..a66162964 --- /dev/null +++ b/src/components/ui/index.ts @@ -0,0 +1,2 @@ +export { Logo } from './Logo'; +export { Modal } from './Modal'; \ No newline at end of file diff --git a/webpack.config.js b/webpack.config.js index 583dfd1ed..067e8a0fb 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -32,7 +32,7 @@ module.exports = (_, args) => { output: { path: dist, publicPath: - args.mode === 'development' ? `http://${host}:${port}/` : undefined /* <- прописать данные своего github */, + args.mode === 'development' ? `http://${host}:${port}/` : './' /* <- прописать данные своего github */, filename: `js/[name].js`, chunkFilename: `js/[name].js`, },