From bd30c23c6609fe49803b21fdd4d3d59828ee26b2 Mon Sep 17 00:00:00 2001 From: Igor Aralov Date: Sat, 31 Aug 2024 16:53:19 +0300 Subject: [PATCH 01/27] Add few words about myself --- src/app/App.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/app/App.tsx b/src/app/App.tsx index dcc0ff8ad..c430f4b9f 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -8,7 +8,11 @@ function App() {
logo

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

From bd497f7f83b79c0e9d00d3a55dcc4fe54be317fb Mon Sep 17 00:00:00 2001 From: Igor Aralov Date: Sat, 31 Aug 2024 19:24:51 +0300 Subject: [PATCH 02/27] Make ordered list --- src/app/App.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/App.tsx b/src/app/App.tsx index c430f4b9f..b109a531a 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -8,11 +8,11 @@ function App() {
logo

-

    +
    1. Научиться разрабатывать веб-приложения при помощи React
    2. Профессионально работаю SQL разработчиком уже 10 лет и принимаю участие в разработке десктопного приложения на Delphi
    3. Интересуюсь фронтенд и фуллстек разработкой на js/ts
    4. -
+

From 0102bedeb7a4ec620cd28ee37153808bfed80303 Mon Sep 17 00:00:00 2001 From: Igor Aralov Date: Sat, 31 Aug 2024 19:59:18 +0300 Subject: [PATCH 03/27] Add demo GitHub actions --- .github/workflows/github-actions-demo.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .github/workflows/github-actions-demo.yml diff --git a/.github/workflows/github-actions-demo.yml b/.github/workflows/github-actions-demo.yml new file mode 100644 index 000000000..15a61d6b6 --- /dev/null +++ b/.github/workflows/github-actions-demo.yml @@ -0,0 +1,18 @@ +name: GitHub Actions Demo +run-name: ${{ github.actor }} is testing out GitHub Actions 🚀 +on: [push] +jobs: + Explore-GitHub-Actions: + runs-on: ubuntu-latest + steps: + - run: echo "🎉 The job was automatically triggered by a ${{ github.event_name }} event." + - run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by GitHub!" + - run: echo "🔎 The name of your branch is ${{ github.ref }} and your repository is ${{ github.repository }}." + - name: Check out repository code + uses: actions/checkout@v4 + - run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner." + - run: echo "🖥️ The workflow is now ready to test your code on the runner." + - name: List files in the repository + run: | + ls ${{ github.workspace }} + - run: echo "🍏 This job's status is ${{ job.status }}." From 8b5809b50fae502d1be162b46b2f8ef1364d3c72 Mon Sep 17 00:00:00 2001 From: Igor Aralov Date: Sat, 31 Aug 2024 20:14:47 +0300 Subject: [PATCH 04/27] Remove demo github actions Modify main page --- .github/workflows/github-actions-demo.yml | 18 ------------------ src/app/App.tsx | 11 ++++++----- 2 files changed, 6 insertions(+), 23 deletions(-) delete mode 100644 .github/workflows/github-actions-demo.yml diff --git a/.github/workflows/github-actions-demo.yml b/.github/workflows/github-actions-demo.yml deleted file mode 100644 index 15a61d6b6..000000000 --- a/.github/workflows/github-actions-demo.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: GitHub Actions Demo -run-name: ${{ github.actor }} is testing out GitHub Actions 🚀 -on: [push] -jobs: - Explore-GitHub-Actions: - runs-on: ubuntu-latest - steps: - - run: echo "🎉 The job was automatically triggered by a ${{ github.event_name }} event." - - run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by GitHub!" - - run: echo "🔎 The name of your branch is ${{ github.ref }} and your repository is ${{ github.repository }}." - - name: Check out repository code - uses: actions/checkout@v4 - - run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner." - - run: echo "🖥️ The workflow is now ready to test your code on the runner." - - name: List files in the repository - run: | - ls ${{ github.workspace }} - - run: echo "🍏 This job's status is ${{ job.status }}." diff --git a/src/app/App.tsx b/src/app/App.tsx index b109a531a..6351692cf 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -8,11 +8,12 @@ function App() {
logo

-

    -
  1. Научиться разрабатывать веб-приложения при помощи React
  2. -
  3. Профессионально работаю SQL разработчиком уже 10 лет и принимаю участие в разработке десктопного приложения на Delphi
  4. -
  5. Интересуюсь фронтенд и фуллстек разработкой на js/ts
  6. -
+

Научиться разрабатывать веб-приложения при помощи React

+

+ Профессионально работаю SQL разработчиком уже 10 лет и принимаю участие в разработке десктопного приложения + на Delphi +

+

Интересуюсь фронтенд и фуллстек разработкой на js/ts

From f319bec4dc05056ffd569acf8e51175b492d4181 Mon Sep 17 00:00:00 2001 From: Igor Aralov Date: Sat, 31 Aug 2024 21:10:21 +0300 Subject: [PATCH 05/27] Add contact information --- src/app/App.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/App.tsx b/src/app/App.tsx index 6351692cf..94a44b7bf 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -8,6 +8,7 @@ function App() {
logo

+

Игорь Аралов (igor.aralov@rambler.ru)

Научиться разрабатывать веб-приложения при помощи React

Профессионально работаю SQL разработчиком уже 10 лет и принимаю участие в разработке десктопного приложения From 463404d24c536e8dae77d5fa1a2f7044a57b2cc3 Mon Sep 17 00:00:00 2001 From: Igor Aralov Date: Tue, 17 Sep 2024 15:13:35 +0300 Subject: [PATCH 06/27] homework1 --- src/homeworks/ts1/1_base.ts | 89 ++++++++ src/homeworks/ts1/2_repair.ts | 86 ++++---- src/homeworks/ts1/3_write.ts | 97 +++++++++ src/homeworks/ts1/data.ts | 400 ++++++++++++++++++++++++++++++++++ 4 files changed, 630 insertions(+), 42 deletions(-) create mode 100644 src/homeworks/ts1/1_base.ts create mode 100644 src/homeworks/ts1/data.ts diff --git a/src/homeworks/ts1/1_base.ts b/src/homeworks/ts1/1_base.ts new file mode 100644 index 000000000..6442464b1 --- /dev/null +++ b/src/homeworks/ts1/1_base.ts @@ -0,0 +1,89 @@ +/** + * Нужно превратить файл в ts и указать типы аргументов и типы возвращаемого значения + * */ +export const removePlus = (string: string): string => string.replace(/^\+/, ''); + +export const addPlus = (string: string): string => `+${string}`; + +export const removeFirstZeros = (value: string): string => value.replace(/^(-)?[0]+(-?\d+.*)$/, '$1$2'); + +export const getBeautifulNumber = (value?: number | null, separator: string = ' ') => + value?.toString().replace(/\B(?=(\d{3})+(?!\d))/g, separator); + +export const round = (value: number, accuracy: number = 2): number => { + const d = 10 ** accuracy; + return Math.round(value * d) / d; +}; + +const transformRegexp: RegExp = + /(matrix\(-?\d+(\.\d+)?, -?\d+(\.\d+)?, -?\d+(\.\d+)?, -?\d+(\.\d+)?, )(-?\d+(\.\d+)?), (-?\d+(\.\d+)?)\)/; + +type TransformedRegexp = { + x: number; + y: number; +}; + +export const getTransformFromCss = (transformCssString: string): TransformedRegexp => { + const data = transformCssString.match(transformRegexp); + if (!data) return { x: 0, y: 0 }; + return { + x: parseInt(data[6], 10), + y: parseInt(data[8], 10), + }; +}; + +type Colors = [red: number, green: number, blue: number]; + +export const getColorContrastValue = ([red, green, blue]: Colors): number => + // http://www.w3.org/TR/AERT#color-contrast + Math.round((red * 299 + green * 587 + blue * 114) / 1000); + +type BlackOrWhite = 'black' | 'white'; + +export const getContrastType = (contrastValue: number): BlackOrWhite => (contrastValue > 125 ? 'black' : 'white'); + +export const shortColorRegExp: RegExp = /^#[0-9a-f]{3}$/i; +export const longColorRegExp: RegExp = /^#[0-9a-f]{6}$/i; + +export const checkColor = (color: string): void | never => { + if (!longColorRegExp.test(color) && !shortColorRegExp.test(color)) throw new Error(`invalid hex color: ${color}`); +}; + +export const hex2rgb = (color: string): Colors => { + checkColor(color); + if (shortColorRegExp.test(color)) { + const red = parseInt(color.substring(1, 2), 16); + const green = parseInt(color.substring(2, 3), 16); + const blue = parseInt(color.substring(3, 4), 16); + return [red, green, blue]; + } + const red = parseInt(color.substring(1, 3), 16); + const green = parseInt(color.substring(3, 5), 16); + const blue = parseInt(color.substring(5, 8), 16); + return [red, green, blue]; +}; + +type NumberedArrayItem = { + value: unknown; + number: number; +}; + +export const getNumberedArray = (arr: unknown[]): NumberedArrayItem[] => + arr.map((value, number) => ({ value, number })); +export const toStringArray = (arr: NumberedArrayItem[]) => arr.map(({ value, number }) => `${value}_${number}`); + +type Customer = { + id: number; + name: string; + age: number; + isSubscribed: boolean; +}; + +type TransformedCustomer = Record>; + +export const transformCustomers = (customers: Customer[]): TransformedCustomer => { + return customers.reduce((acc: TransformedCustomer, customer) => { + acc[customer.id] = { name: customer.name, age: customer.age, isSubscribed: customer.isSubscribed }; + return acc; + }, {}); +}; diff --git a/src/homeworks/ts1/2_repair.ts b/src/homeworks/ts1/2_repair.ts index 19e98c068..3c9b28d00 100644 --- a/src/homeworks/ts1/2_repair.ts +++ b/src/homeworks/ts1/2_repair.ts @@ -2,46 +2,48 @@ * Здесь код с ошибками типов. Нужно их устранить * */ -// // Мы это не проходили, но по тексту ошибки можно понять, как это починить -// export const getFakeApi = async (): void => { -// const result = await fetch('https://jsonplaceholder.typicode.com/todos/1').then((response) => response.json()); -// console.log(result); -// }; +// Мы это не проходили, но по тексту ошибки можно понять, как это починить +export const getFakeApi = async (): Promise => { + const result = await fetch('https://jsonplaceholder.typicode.com/todos/1').then((response) => response.json()); + console.log(result); +}; // -// // Мы это не проходили, но по тексту ошибки можно понять, как это починить -// export class SomeClass { -// constructor() { -// this.set = new Set([1]); -// this.channel = new BroadcastChannel('test-broadcast-channel'); -// } -// } -// -// export type Data = { -// type: 'Money' | 'Percent'; -// value: DataValue; -// }; -// -// export type DataValue = Money | Percent; -// -// export type Money = { -// currency: string; -// amount: number; -// }; -// -// export type Percent = { -// percent: number; -// }; -// -// // Здесь, возможно, нужно использовать as, возможно в switch передавать немного по-другому -// const getDataAmount = (data: Data): number => { -// switch (data.type) { -// case 'Money': -// return data.value.amount; -// -// default: { -// // eslint-disable-next-line @typescript-eslint/no-unused-vars -// const unhandled: never = data; // здесь, возможно, нужно использовать нечто другое. :never должен остаться -// throw new Error(`unknown type: ${data.type}`); -// } -// } -// }; +// Мы это не проходили, но по тексту ошибки можно понять, как это починить +export class SomeClass { + readonly set: Set; + readonly channel: BroadcastChannel; + constructor() { + this.set = new Set([1]); + this.channel = new BroadcastChannel('test-broadcast-channel'); + } +} + +export type Money = { + currency: string; + amount: number; +}; + +export type Percent = { + percent: number; +}; + +export type DataValue = Money | Percent; + +export type Data = { + type: 'Money' | 'Percent'; + value: DataValue; +}; + +// Здесь, возможно, нужно использовать as, возможно в switch передавать немного по-другому +const getDataAmount = (data: Data): number | never => { + switch (data.type) { + case 'Money': + return (data.value as Money).amount; + + default: { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const unhandled: never = data as never; // здесь, возможно, нужно использовать нечто другое. :never должен остаться + throw new Error(`unknown type: ${data.type}`); + } + } +}; diff --git a/src/homeworks/ts1/3_write.ts b/src/homeworks/ts1/3_write.ts index 15f9dcdf2..d94010070 100644 --- a/src/homeworks/ts1/3_write.ts +++ b/src/homeworks/ts1/3_write.ts @@ -4,6 +4,103 @@ * Поэтому в идеале чтобы функции возвращали случайные данные, но в то же время не абракадабру. * В целом сделайте так, как вам будет удобно. * */ +import crypto from 'crypto'; +import { names, photos, nouns, adjectives } from './data'; + +type Category = { + id: string; + name: string; + photo?: string; +}; + +type Product = { + id: string; + name: string; + photo: string; + desc?: string; + createdAt: string; + oldPrice?: number; + price: number; + category: Category; +}; + +type Operation = Cost | Profit; + +type Cost = { + id: string; + name: string; + desc?: string; + createdAt: string; + amount: number; + category: Category; + type: 'Cost'; +}; + +type Profit = { + id: string; + name: string; + desc?: string; + createdAt: string; + amount: number; + category: Category; + type: 'Profit'; +}; + +const getRandomItemFromArray = (arr: T[]): T => arr[Math.floor(Math.random() * arr.length)]; +const getRandomNumber = (min: number, max: number): number => Math.floor(Math.random() * (max - min + 1)) + min; +const getRandomDescription = (nouns: string[], adjectives: string[]): string => { + const fourAdjectives = [...Array(4)].map(() => getRandomItemFromArray(adjectives)).join(' '); + const noun = getRandomItemFromArray(nouns); + return `${fourAdjectives} ${noun}`; +}; +const getRandomId = crypto.randomUUID; +const createRandomCategory = (): Category => ({ + id: getRandomId(), + name: getRandomItemFromArray(names), + photo: getRandomItemFromArray(photos), +}); + +const createRandomCost = (createdAt: string): Cost => ({ + id: getRandomId(), + name: getRandomItemFromArray(names), + desc: getRandomDescription(nouns, adjectives), + createdAt, + amount: getRandomNumber(100, 1000), + category: createRandomCategory(), + type: 'Cost', +}); + +const createRandomProfit = (createdAt: string): Profit => ({ + id: getRandomId(), + name: getRandomItemFromArray(names), + desc: getRandomDescription(nouns, adjectives), + createdAt, + amount: getRandomNumber(100, 1000), + category: createRandomCategory(), + type: 'Profit', +}); + +export const createRandomOperation = (createdAt: string): Operation => { + type OperationsMap = Record Operation>; + const operationsMap: OperationsMap = { + 0: createRandomCost, + 1: createRandomProfit, + }; + const randomNumber = getRandomNumber(0, Object.keys(operationsMap).length - 1); + + return operationsMap[randomNumber](createdAt); +}; + +export const createRandomProduct = (createdAt: string): Product => ({ + id: getRandomId(), + name: getRandomItemFromArray(names), + photo: getRandomItemFromArray(photos), + desc: getRandomDescription(nouns, adjectives), + createdAt, + oldPrice: getRandomNumber(100, 1000), + price: getRandomNumber(100, 1000), + category: createRandomCategory(), +}); /** * Нужно создать тип Category, он будет использоваться ниже. diff --git a/src/homeworks/ts1/data.ts b/src/homeworks/ts1/data.ts new file mode 100644 index 000000000..9bd520d32 --- /dev/null +++ b/src/homeworks/ts1/data.ts @@ -0,0 +1,400 @@ +export const names = [ + 'Emma', + 'Liam', + 'Olivia', + 'Noah', + 'Ava', + 'William', + 'Sophia', + 'James', + 'Isabella', + 'Benjamin', + 'Mia', + 'Lucas', + 'Charlotte', + 'Henry', + 'Amelia', + 'Alexander', + 'Harper', + 'Michael', + 'Evelyn', + 'Ethan', + 'Abigail', + 'Daniel', + 'Ella', + 'Matthew', + 'Avery', + 'Aiden', + 'Scarlett', + 'Jackson', + 'Grace', + 'Logan', + 'Chloe', + 'David', + 'Victoria', + 'Joseph', + 'Riley', + 'Samuel', + 'Aria', + 'Sebastian', + 'Lily', + 'Jack', + 'Zoey', + 'John', + 'Mila', + 'Owen', + 'Layla', + 'Gabriel', + 'Nora', + 'Carter', + 'Ellie', + 'Luke', + 'Madison', + 'Anthony', + 'Hazel', + 'Isaac', + 'Aurora', + 'Dylan', + 'Penelope', + 'Wyatt', + 'Lillian', + 'Andrew', + 'Addison', + 'Joshua', + 'Lucy', + 'Christopher', + 'Hannah', + 'Grayson', + 'Stella', + 'Jack', + 'Natalie', + 'Julian', + 'Leah', + 'Ryan', + 'Violet', + 'Jaxon', + 'Savannah', + 'Levi', + 'Audrey', + 'Nathan', + 'Brooklyn', + 'Caleb', + 'Bella', + 'Christian', + 'Claire', + 'Hunter', + 'Skylar', + 'Eli', + 'Samantha', + 'Isaiah', + 'Paisley', + 'Thomas', + 'Kennedy', + 'Charles', + 'Ellie', + 'Aaron', + 'Peyton', + 'Lincoln', + 'Mila', + 'Adrian', + 'Sophie', + 'Jonathan', +]; + +export const photos = [ + 'https://example.com/page1', + 'https://example.com/page2', + 'https://example.com/page3', + 'https://example.com/page4', + 'https://example.com/page5', + 'https://example.com/page6', + 'https://example.com/page7', + 'https://example.com/page8', + 'https://example.com/page9', + 'https://example.com/page10', + 'https://example.com/page11', + 'https://example.com/page12', + 'https://example.com/page13', + 'https://example.com/page14', + 'https://example.com/page15', + 'https://example.com/page16', + 'https://example.com/page17', + 'https://example.com/page18', + 'https://example.com/page19', + 'https://example.com/page20', + 'https://example.com/page21', + 'https://example.com/page22', + 'https://example.com/page23', + 'https://example.com/page24', + 'https://example.com/page25', + 'https://example.com/page26', + 'https://example.com/page27', + 'https://example.com/page28', + 'https://example.com/page29', + 'https://example.com/page30', + 'https://example.com/page31', + 'https://example.com/page32', + 'https://example.com/page33', + 'https://example.com/page34', + 'https://example.com/page35', + 'https://example.com/page36', + 'https://example.com/page37', + 'https://example.com/page38', + 'https://example.com/page39', + 'https://example.com/page40', + 'https://example.com/page41', + 'https://example.com/page42', + 'https://example.com/page43', + 'https://example.com/page44', + 'https://example.com/page45', + 'https://example.com/page46', + 'https://example.com/page47', + 'https://example.com/page48', + 'https://example.com/page49', + 'https://example.com/page50', + 'https://example.com/page51', + 'https://example.com/page52', + 'https://example.com/page53', + 'https://example.com/page54', + 'https://example.com/page55', + 'https://example.com/page56', + 'https://example.com/page57', + 'https://example.com/page58', + 'https://example.com/page59', + 'https://example.com/page60', + 'https://example.com/page61', + 'https://example.com/page62', + 'https://example.com/page63', + 'https://example.com/page64', + 'https://example.com/page65', + 'https://example.com/page66', + 'https://example.com/page67', + 'https://example.com/page68', + 'https://example.com/page69', + 'https://example.com/page70', + 'https://example.com/page71', + 'https://example.com/page72', + 'https://example.com/page73', + 'https://example.com/page74', + 'https://example.com/page75', + 'https://example.com/page76', + 'https://example.com/page77', + 'https://example.com/page78', + 'https://example.com/page79', + 'https://example.com/page80', + 'https://example.com/page81', + 'https://example.com/page82', + 'https://example.com/page83', + 'https://example.com/page84', + 'https://example.com/page85', + 'https://example.com/page86', + 'https://example.com/page87', + 'https://example.com/page88', + 'https://example.com/page89', + 'https://example.com/page90', + 'https://example.com/page91', + 'https://example.com/page92', + 'https://example.com/page93', + 'https://example.com/page94', + 'https://example.com/page95', + 'https://example.com/page96', + 'https://example.com/page97', + 'https://example.com/page98', + 'https://example.com/page99', + 'https://example.com/page100', +]; + +export const adjectives = [ + 'happy', + 'sad', + 'angry', + 'excited', + 'bored', + 'tired', + 'energetic', + 'calm', + 'anxious', + 'brave', + 'curious', + 'daring', + 'eager', + 'fearful', + 'gentle', + 'honest', + 'jolly', + 'kind', + 'lazy', + 'mysterious', + 'nervous', + 'optimistic', + 'pessimistic', + 'quiet', + 'restless', + 'silly', + 'thoughtful', + 'upbeat', + 'victorious', + 'witty', + 'zealous', + 'adventurous', + 'bold', + 'cheerful', + 'determined', + 'elegant', + 'friendly', + 'graceful', + 'humble', + 'intelligent', + 'joyful', + 'keen', + 'loyal', + 'modest', + 'noble', + 'outgoing', + 'polite', + 'quick', + 'reliable', + 'sincere', + 'trustworthy', + 'understanding', + 'vibrant', + 'wise', + 'youthful', + 'zany', + 'ambitious', + 'brilliant', + 'creative', + 'diligent', + 'enthusiastic', + 'forgiving', + 'generous', + 'helpful', + 'imaginative', + 'jovial', + 'knowledgeable', + 'loving', + 'motivated', + 'neat', + 'observant', + 'patient', + 'quirky', + 'respectful', + 'supportive', + 'talented', + 'unique', + 'versatile', + 'warm', + 'xenial', + 'yielding', + 'zealous', + 'affectionate', + 'bright', + 'courageous', + 'dependable', + 'empathetic', + 'faithful', + 'gentle', + 'hardworking', + 'innovative', + 'jubilant', + 'kindhearted', + 'likable', + 'mindful', + 'nurturing', + 'optimistic', + 'persistent', + 'resourceful', + 'sensible', +]; +export const nouns = [ + 'apple', + 'banana', + 'car', + 'dog', + 'elephant', + 'flower', + 'guitar', + 'house', + 'island', + 'jacket', + 'kite', + 'lamp', + 'mountain', + 'notebook', + 'ocean', + 'pencil', + 'queen', + 'river', + 'sun', + 'tree', + 'umbrella', + 'vase', + 'whale', + 'xylophone', + 'yacht', + 'zebra', + 'airplane', + 'book', + 'cat', + 'door', + 'engine', + 'forest', + 'garden', + 'hat', + 'ice', + 'jungle', + 'key', + 'lion', + 'moon', + 'nest', + 'orange', + 'piano', + 'quilt', + 'road', + 'star', + 'train', + 'unicorn', + 'violin', + 'window', + 'x-ray', + 'yogurt', + 'zoo', + 'ant', + 'ball', + 'cake', + 'desk', + 'egg', + 'fish', + 'goat', + 'hill', + 'igloo', + 'jewel', + 'kangaroo', + 'leaf', + 'mirror', + 'net', + 'owl', + 'pizza', + 'quill', + 'ring', + 'ship', + 'table', + 'umbrella', + 'village', + 'water', + 'xenon', + 'yarn', + 'zinc', + 'arch', + 'bridge', + 'cloud', + 'drum', + 'earth', + 'feather', + 'glove', + 'hammer', + 'ink', + 'jigsaw', + 'kite', + 'ladder', +]; From 2cab36f70dd130add7ffda1f7b98de1b3c47c713 Mon Sep 17 00:00:00 2001 From: Igor Aralov Date: Tue, 24 Sep 2024 21:31:42 +0300 Subject: [PATCH 07/27] Fix minor flaws --- src/homeworks/ts1/1_base.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/homeworks/ts1/1_base.ts b/src/homeworks/ts1/1_base.ts index 6442464b1..60d9ce278 100644 --- a/src/homeworks/ts1/1_base.ts +++ b/src/homeworks/ts1/1_base.ts @@ -7,7 +7,7 @@ export const addPlus = (string: string): string => `+${string}`; export const removeFirstZeros = (value: string): string => value.replace(/^(-)?[0]+(-?\d+.*)$/, '$1$2'); -export const getBeautifulNumber = (value?: number | null, separator: string = ' ') => +export const getBeautifulNumber = (value: number, separator = ' '): string => value?.toString().replace(/\B(?=(\d{3})+(?!\d))/g, separator); export const round = (value: number, accuracy: number = 2): number => { @@ -63,14 +63,14 @@ export const hex2rgb = (color: string): Colors => { return [red, green, blue]; }; -type NumberedArrayItem = { - value: unknown; +type NumberedArrayItem = { + value: T; number: number; }; -export const getNumberedArray = (arr: unknown[]): NumberedArrayItem[] => +export const getNumberedArray = (arr: T[]): NumberedArrayItem[] => arr.map((value, number) => ({ value, number })); -export const toStringArray = (arr: NumberedArrayItem[]) => arr.map(({ value, number }) => `${value}_${number}`); +export const toStringArray = (arr: NumberedArrayItem[]) => arr.map(({ value, number }) => `${value}_${number}`); type Customer = { id: number; From 6716a89891c4e190fc65b0eaf43eee52b2927ea2 Mon Sep 17 00:00:00 2001 From: Igor Aralov Date: Tue, 24 Sep 2024 21:50:31 +0300 Subject: [PATCH 08/27] Deploy Storybook to Github Pages --- .github/workflows/main.yml | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0f5ba90b0..fcd99394f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -35,27 +35,27 @@ jobs: run: npm run lint && npm test # Собираем приложение - - name: Build Application - run: npm run build + # - name: Build Application + # run: npm run build # Публикуем приложение на Github Pages - - name: Deploy to 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 From 2065e9e0646daf62b67c309dc8cf65e8874e2bb3 Mon Sep 17 00:00:00 2001 From: Igor Aralov Date: Sun, 24 Nov 2024 19:01:43 +0300 Subject: [PATCH 09/27] homework 2,3 --- src/shared/button/Button.modules.scss | 14 + src/shared/button/Button.tsx | 40 +-- src/shared/button/button.css | 30 -- src/shared/button/sum.test.ts | 5 - src/shared/button/sum.ts | 1 - .../description/Description.module.scss | 11 + src/shared/description/Description.tsx | 12 + src/shared/header/Header.module.scss | 10 + src/shared/header/Header.tsx | 9 + src/shared/i-shop/AddToCart.module.scss | 14 + src/shared/i-shop/AddToCart.tsx | 18 + src/shared/i-shop/Product.module.scss | 10 + src/shared/i-shop/Product.tsx | 10 + src/shared/i-shop/ProductCart.module.scss | 11 + src/shared/i-shop/ProductCart.tsx | 14 + .../i-shop/ProductContainer.module.scss | 9 + src/shared/i-shop/ProductContainer.tsx | 7 + src/shared/i-shop/ProductDetail.module.scss | 13 + src/shared/i-shop/ProductDetail.tsx | 25 ++ src/shared/i-shop/ProductSummary.tsx | 19 ++ src/shared/i-shop/product.types.ts | 25 ++ .../Operation.module.scss | 21 ++ .../icome-expenses-accounting/Operation.tsx | 11 + .../OperationContainer.module.scss | 8 + .../OperationContainer.tsx | 7 + .../OperationDetail.module.scss | 5 + .../OperationDetail.tsx | 23 ++ .../OperationSummary.tsx | 12 + .../operation.types.ts | 17 + src/shared/image/Image.module.scss | 5 + src/shared/image/Image.tsx | 9 + src/shared/layout/Layout.module.scss | 5 + src/shared/layout/Layout.tsx | 315 ++++++++++++++++++ src/shared/logo/Logo.module.scss | 13 + src/shared/logo/Logo.tsx | 4 + src/shared/modal-form/ModalForm.module.scss | 37 ++ src/shared/modal-form/ModalForm.tsx | 19 ++ src/shared/mt15/MT15.modules.scss | 3 + src/shared/mt15/MT15.tsx | 8 + src/stories/AddToCart.stories.ts | 17 + src/stories/Button.stories.ts | 43 --- src/stories/Button.tsx | 42 --- src/stories/Header.stories.ts | 21 +- src/stories/Header.tsx | 49 --- src/stories/Layout.stories.ts | 13 + src/stories/Logo.stories.ts | 13 + src/stories/ModalForm.stories.tsx | 52 +++ src/stories/OperationDetail.stories.tsx | 22 ++ src/stories/OperationSummary.stories.tsx | 21 ++ src/stories/Page.stories.ts | 29 -- src/stories/Page.tsx | 70 ---- src/stories/ProductCart.stories.ts | 20 ++ src/stories/ProductDetail.stories.ts | 22 ++ src/stories/ProductSummary.stories.ts | 21 ++ src/stories/button.module.sass | 30 -- src/stories/header.css | 32 -- src/stories/page.css | 69 ---- 57 files changed, 969 insertions(+), 446 deletions(-) create mode 100644 src/shared/button/Button.modules.scss delete mode 100644 src/shared/button/button.css delete mode 100644 src/shared/button/sum.test.ts delete mode 100644 src/shared/button/sum.ts create mode 100644 src/shared/description/Description.module.scss create mode 100644 src/shared/description/Description.tsx create mode 100644 src/shared/header/Header.module.scss create mode 100644 src/shared/header/Header.tsx create mode 100644 src/shared/i-shop/AddToCart.module.scss create mode 100644 src/shared/i-shop/AddToCart.tsx create mode 100644 src/shared/i-shop/Product.module.scss create mode 100644 src/shared/i-shop/Product.tsx create mode 100644 src/shared/i-shop/ProductCart.module.scss create mode 100644 src/shared/i-shop/ProductCart.tsx create mode 100644 src/shared/i-shop/ProductContainer.module.scss create mode 100644 src/shared/i-shop/ProductContainer.tsx create mode 100644 src/shared/i-shop/ProductDetail.module.scss create mode 100644 src/shared/i-shop/ProductDetail.tsx create mode 100644 src/shared/i-shop/ProductSummary.tsx create mode 100644 src/shared/i-shop/product.types.ts create mode 100644 src/shared/icome-expenses-accounting/Operation.module.scss create mode 100644 src/shared/icome-expenses-accounting/Operation.tsx create mode 100644 src/shared/icome-expenses-accounting/OperationContainer.module.scss create mode 100644 src/shared/icome-expenses-accounting/OperationContainer.tsx create mode 100644 src/shared/icome-expenses-accounting/OperationDetail.module.scss create mode 100644 src/shared/icome-expenses-accounting/OperationDetail.tsx create mode 100644 src/shared/icome-expenses-accounting/OperationSummary.tsx create mode 100644 src/shared/icome-expenses-accounting/operation.types.ts create mode 100644 src/shared/image/Image.module.scss create mode 100644 src/shared/image/Image.tsx create mode 100644 src/shared/layout/Layout.module.scss create mode 100644 src/shared/layout/Layout.tsx create mode 100644 src/shared/logo/Logo.module.scss create mode 100644 src/shared/logo/Logo.tsx create mode 100644 src/shared/modal-form/ModalForm.module.scss create mode 100644 src/shared/modal-form/ModalForm.tsx create mode 100644 src/shared/mt15/MT15.modules.scss create mode 100644 src/shared/mt15/MT15.tsx create mode 100644 src/stories/AddToCart.stories.ts delete mode 100644 src/stories/Button.stories.ts delete mode 100644 src/stories/Button.tsx delete mode 100644 src/stories/Header.tsx create mode 100644 src/stories/Layout.stories.ts create mode 100644 src/stories/Logo.stories.ts create mode 100644 src/stories/ModalForm.stories.tsx create mode 100644 src/stories/OperationDetail.stories.tsx create mode 100644 src/stories/OperationSummary.stories.tsx delete mode 100644 src/stories/Page.stories.ts delete mode 100644 src/stories/Page.tsx create mode 100644 src/stories/ProductCart.stories.ts create mode 100644 src/stories/ProductDetail.stories.ts create mode 100644 src/stories/ProductSummary.stories.ts delete mode 100644 src/stories/button.module.sass delete mode 100644 src/stories/header.css delete mode 100644 src/stories/page.css diff --git a/src/shared/button/Button.modules.scss b/src/shared/button/Button.modules.scss new file mode 100644 index 000000000..b476ee8aa --- /dev/null +++ b/src/shared/button/Button.modules.scss @@ -0,0 +1,14 @@ +.button { + padding: 10px 15px; + font-size: 1em; + color: #fff; + background-color: #007bff; + border: none; + border-radius: 5px; + cursor: pointer; +} + +.disabled { + cursor: not-allowed; + opacity: 0.6; +} diff --git a/src/shared/button/Button.tsx b/src/shared/button/Button.tsx index 7548da044..fb8c9ef44 100644 --- a/src/shared/button/Button.tsx +++ b/src/shared/button/Button.tsx @@ -1,34 +1,12 @@ -import React, { FC } from 'react'; +import React, { ReactNode } from 'react'; import cn from 'clsx'; -import { sum } from './sum'; -import './button.css'; +import s from './Button.modules.scss'; -interface ButtonProps { - primary?: boolean; - backgroundColor?: string | null; - size?: string; - label: string; -} -/** - * Primary UI component for user interaction - */ - -export const Button: FC = ({ primary, backgroundColor, size, label, ...props }) => { - const mode = primary ? 'storybook-button--primary' : 'storybook-button--secondary'; - - const onClick = () => { - sum(4, 5); - }; - - return ( - - ); +type ButtonProps = { + children: ReactNode; + disabled?: boolean; }; + +export const Button = ({ children, disabled = false }: ButtonProps) => ( + +); diff --git a/src/shared/button/button.css b/src/shared/button/button.css deleted file mode 100644 index 61c9c11f3..000000000 --- a/src/shared/button/button.css +++ /dev/null @@ -1,30 +0,0 @@ -.storybook-button { - font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; - font-weight: 700; - border: 0; - border-radius: 3em; - cursor: pointer; - display: inline-block; - line-height: 1; -} -.storybook-button--primary { - color: white; - background-color: #1ea7fd; -} -.storybook-button--secondary { - color: #333; - background-color: transparent; - box-shadow: rgba(0, 0, 0, 0.15) 0px 0px 0px 1px inset; -} -.storybook-button--small { - font-size: 12px; - padding: 10px 16px; -} -.storybook-button--medium { - font-size: 14px; - padding: 11px 20px; -} -.storybook-button--large { - font-size: 16px; - padding: 12px 24px; -} diff --git a/src/shared/button/sum.test.ts b/src/shared/button/sum.test.ts deleted file mode 100644 index b0c67eb9b..000000000 --- a/src/shared/button/sum.test.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { sum } from './sum'; - -test('adds 4 + 5 to equal 9', () => { - expect(sum(4, 5)).toBe(9); -}); diff --git a/src/shared/button/sum.ts b/src/shared/button/sum.ts deleted file mode 100644 index 8073d27cc..000000000 --- a/src/shared/button/sum.ts +++ /dev/null @@ -1 +0,0 @@ -export const sum = (a: number, b: number): number => a + b; diff --git a/src/shared/description/Description.module.scss b/src/shared/description/Description.module.scss new file mode 100644 index 000000000..3fe4f8011 --- /dev/null +++ b/src/shared/description/Description.module.scss @@ -0,0 +1,11 @@ +.description { + font-size: 1em; + color: #555; + margin-top: 10px; +} + +.short { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} diff --git a/src/shared/description/Description.tsx b/src/shared/description/Description.tsx new file mode 100644 index 000000000..9e0b570bb --- /dev/null +++ b/src/shared/description/Description.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import cn from 'clsx'; +import s from './Description.module.scss'; + +type Description = { + description: string; + isShort?: boolean; +}; + +export const Description = ({ description, isShort = false }: Description) => ( +

{description}
+); diff --git a/src/shared/header/Header.module.scss b/src/shared/header/Header.module.scss new file mode 100644 index 000000000..9d699ac7c --- /dev/null +++ b/src/shared/header/Header.module.scss @@ -0,0 +1,10 @@ +.header { + position: -webkit-sticky; + position: sticky; + top: 0; + background-color: #333; + color: white; + padding: 10px; + min-height: 50px; + z-index: 1000; +} diff --git a/src/shared/header/Header.tsx b/src/shared/header/Header.tsx new file mode 100644 index 000000000..3fb6135f7 --- /dev/null +++ b/src/shared/header/Header.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import { Logo } from '../logo/Logo'; +import s from './Header.module.scss'; + +type HeaderProps = { + showLogo?: boolean; +}; + +export const Header = ({ showLogo = true }: HeaderProps) =>
{showLogo && }
; diff --git a/src/shared/i-shop/AddToCart.module.scss b/src/shared/i-shop/AddToCart.module.scss new file mode 100644 index 000000000..bdf6bce42 --- /dev/null +++ b/src/shared/i-shop/AddToCart.module.scss @@ -0,0 +1,14 @@ +.quantity-container { + display: flex; + align-items: center; +} + +.quantity-input { + width: 50px; + font-size: 1em; + text-align: center; + border: 1px solid #007bff; + border-radius: 5px; + margin: 0 5px; + padding: 5px; +} diff --git a/src/shared/i-shop/AddToCart.tsx b/src/shared/i-shop/AddToCart.tsx new file mode 100644 index 000000000..a5cddbce5 --- /dev/null +++ b/src/shared/i-shop/AddToCart.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { Button } from '../button/Button'; +import s from './AddToCart.module.scss'; + +type AddToCartProps = { + quantity: number; +}; + +const AddToCartEdit = ({ quantity }: AddToCartProps) => ( +
+ + + +
+); + +export const AddToCart = ({ quantity }: AddToCartProps) => + quantity > 0 ? : ; diff --git a/src/shared/i-shop/Product.module.scss b/src/shared/i-shop/Product.module.scss new file mode 100644 index 000000000..73437aa8b --- /dev/null +++ b/src/shared/i-shop/Product.module.scss @@ -0,0 +1,10 @@ +.product-title { + font-size: 1.5em; + margin: 16px 0; +} + +.product-price { + color: #e91e63; + font-size: 1.2em; + margin: 8px 0; +} diff --git a/src/shared/i-shop/Product.tsx b/src/shared/i-shop/Product.tsx new file mode 100644 index 000000000..0c333a6c9 --- /dev/null +++ b/src/shared/i-shop/Product.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import { Product as ProductProps } from './product.types'; +import s from './Product.module.scss'; + +export const Product = (data: ProductProps) => ( + <> +

{data.title}

+

{`Цена: ${data.price} ₽`}

+ +); diff --git a/src/shared/i-shop/ProductCart.module.scss b/src/shared/i-shop/ProductCart.module.scss new file mode 100644 index 000000000..eebdefd22 --- /dev/null +++ b/src/shared/i-shop/ProductCart.module.scss @@ -0,0 +1,11 @@ +.product-cart { + display: flex; + justify-content: space-between; + align-items: center; + background-color: #fff; + border: 1px solid #ddd; + border-radius: 8px; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); + width: 600px; + padding: 16px; +} diff --git a/src/shared/i-shop/ProductCart.tsx b/src/shared/i-shop/ProductCart.tsx new file mode 100644 index 000000000..e2e533655 --- /dev/null +++ b/src/shared/i-shop/ProductCart.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { ProductCart as ProductCartProps } from './product.types'; +import { Product } from './Product'; +import { Button } from '../button/Button'; +import s from './ProductCart.module.scss'; + +const DeleteButton = () => ; + +export const ProductCart = ({ product }: ProductCartProps) => ( +
+ + +
+); diff --git a/src/shared/i-shop/ProductContainer.module.scss b/src/shared/i-shop/ProductContainer.module.scss new file mode 100644 index 000000000..062ed1bb7 --- /dev/null +++ b/src/shared/i-shop/ProductContainer.module.scss @@ -0,0 +1,9 @@ +.product-container { + background-color: #fff; + border: 1px solid #ddd; + border-radius: 8px; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); + width: 300px; + padding: 16px; + text-align: center; +} diff --git a/src/shared/i-shop/ProductContainer.tsx b/src/shared/i-shop/ProductContainer.tsx new file mode 100644 index 000000000..b519aad14 --- /dev/null +++ b/src/shared/i-shop/ProductContainer.tsx @@ -0,0 +1,7 @@ +import React from 'react'; +import { ProductContainer as ProductContainerProps } from './product.types'; +import s from './ProductContainer.module.scss'; + +export const ProductContainer = ({ children }: ProductContainerProps) => ( +
{children}
+); diff --git a/src/shared/i-shop/ProductDetail.module.scss b/src/shared/i-shop/ProductDetail.module.scss new file mode 100644 index 000000000..eb47a1a35 --- /dev/null +++ b/src/shared/i-shop/ProductDetail.module.scss @@ -0,0 +1,13 @@ +.product-category { + font-size: 1.2em; + color: #888; + margin-bottom: 8px; +} + +.product-images { + display: flex; + flex-direction: column; + justify-content: center; + gap: 8px; + margin-bottom: 16px; +} diff --git a/src/shared/i-shop/ProductDetail.tsx b/src/shared/i-shop/ProductDetail.tsx new file mode 100644 index 000000000..8be16c184 --- /dev/null +++ b/src/shared/i-shop/ProductDetail.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { ProductDetail as ProductDetailProps } from './product.types'; +import { ProductContainer } from './ProductContainer'; +import { Product } from './Product'; +import { Description } from '../description/Description'; +import { AddToCart } from './AddToCart'; +import { Image } from '../image/Image'; +import { MT15 } from '../mt15/MT15'; +import s from './ProductDetail.module.scss'; + +export const ProductDetail = (data: ProductDetailProps) => ( + +
{data.category}
+
+ {data.images.map((image, index) => ( + + ))} +
+ + + + + +
+); diff --git a/src/shared/i-shop/ProductSummary.tsx b/src/shared/i-shop/ProductSummary.tsx new file mode 100644 index 000000000..f46c029e0 --- /dev/null +++ b/src/shared/i-shop/ProductSummary.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { ProductSummary as ProductSummaryProps } from './product.types'; +import { ProductContainer } from './ProductContainer'; +import { Product } from './Product'; +import { Description } from '../description/Description'; +import { AddToCart } from './AddToCart'; +import { Image } from '../image/Image'; +import { MT15 } from '../mt15/MT15'; + +export const ProductSummary = (data: ProductSummaryProps) => ( + + + + + + + + +); diff --git a/src/shared/i-shop/product.types.ts b/src/shared/i-shop/product.types.ts new file mode 100644 index 000000000..5d2854dfb --- /dev/null +++ b/src/shared/i-shop/product.types.ts @@ -0,0 +1,25 @@ +import { ReactNode } from 'react'; + +export type Product = { + title: string; + price: number; +}; + +export type ProductContainer = { + children: ReactNode; +}; + +export type ProductSummary = Product & { + image: string; + shortDescription: string; +}; + +export type ProductDetail = Product & { + images: string[]; + category: string; + description: string; +}; + +export type ProductCart = { + product: Product; +}; diff --git a/src/shared/icome-expenses-accounting/Operation.module.scss b/src/shared/icome-expenses-accounting/Operation.module.scss new file mode 100644 index 000000000..a58796d24 --- /dev/null +++ b/src/shared/icome-expenses-accounting/Operation.module.scss @@ -0,0 +1,21 @@ +.amount { + font-size: 1.5em; + font-weight: bold; + color: #4caf50; + + &::before { + content: '₽'; + } +} + +.category { + font-size: 1.2em; + color: #888; + margin-top: 5px; +} + +.title { + font-size: 1.3em; + font-weight: bold; + margin-top: 10px; +} diff --git a/src/shared/icome-expenses-accounting/Operation.tsx b/src/shared/icome-expenses-accounting/Operation.tsx new file mode 100644 index 000000000..3d1b7073e --- /dev/null +++ b/src/shared/icome-expenses-accounting/Operation.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import { Operation as OperationProps } from './operation.types'; +import s from './Operation.module.scss'; + +export const Operation = (data: OperationProps) => ( + <> +
{data.amount}
+
{data.category}
+
{data.title}
+ +); diff --git a/src/shared/icome-expenses-accounting/OperationContainer.module.scss b/src/shared/icome-expenses-accounting/OperationContainer.module.scss new file mode 100644 index 000000000..08374062b --- /dev/null +++ b/src/shared/icome-expenses-accounting/OperationContainer.module.scss @@ -0,0 +1,8 @@ +.operation-container { + background-color: #fff; + border: 1px solid #ddd; + border-radius: 5px; + padding: 15px; + margin-bottom: 20px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} diff --git a/src/shared/icome-expenses-accounting/OperationContainer.tsx b/src/shared/icome-expenses-accounting/OperationContainer.tsx new file mode 100644 index 000000000..dfefb1769 --- /dev/null +++ b/src/shared/icome-expenses-accounting/OperationContainer.tsx @@ -0,0 +1,7 @@ +import React from 'react'; +import { OperationContainer as OperationContainerProps } from './operation.types'; +import s from './OperationContainer.module.scss'; + +export const OperationContainer = ({ children }: OperationContainerProps) => ( +
{children}
+); diff --git a/src/shared/icome-expenses-accounting/OperationDetail.module.scss b/src/shared/icome-expenses-accounting/OperationDetail.module.scss new file mode 100644 index 000000000..c5947c57f --- /dev/null +++ b/src/shared/icome-expenses-accounting/OperationDetail.module.scss @@ -0,0 +1,5 @@ +.date { + font-size: 0.9em; + color: #999; + margin-top: 10px; +} diff --git a/src/shared/icome-expenses-accounting/OperationDetail.tsx b/src/shared/icome-expenses-accounting/OperationDetail.tsx new file mode 100644 index 000000000..adb57a05c --- /dev/null +++ b/src/shared/icome-expenses-accounting/OperationDetail.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { OperationDetail as OperationDetailProps } from './operation.types'; +import { OperationContainer } from './OperationContainer'; +import { Operation } from './Operation'; +import { Description } from '../description/Description'; +import { Button } from '../button/Button'; +import { MT15 } from '../mt15/MT15'; +import s from './OperationDetail.module.scss'; + +const EditButton = () => ( + + + +); + +export const OperationDetail = (data: OperationDetailProps) => ( + + + +
{data.date}
+ +
+); diff --git a/src/shared/icome-expenses-accounting/OperationSummary.tsx b/src/shared/icome-expenses-accounting/OperationSummary.tsx new file mode 100644 index 000000000..776471604 --- /dev/null +++ b/src/shared/icome-expenses-accounting/OperationSummary.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { OperationSummary as OperationSummaryProps } from './operation.types'; +import { OperationContainer } from './OperationContainer'; +import { Operation } from './Operation'; +import { Description } from '../description/Description'; + +export const OperationSummary = (data: OperationSummaryProps) => ( + + + + +); diff --git a/src/shared/icome-expenses-accounting/operation.types.ts b/src/shared/icome-expenses-accounting/operation.types.ts new file mode 100644 index 000000000..d14b64746 --- /dev/null +++ b/src/shared/icome-expenses-accounting/operation.types.ts @@ -0,0 +1,17 @@ +import { ReactNode } from 'react'; + +export type OperationContainer = { + children: ReactNode; +}; + +export type Operation = { + amount: number; + category: string; + title: string; +}; + +export type OperationSummary = Operation & { + shortDescription: string; +}; + +export type OperationDetail = Operation & { description: string; date: string }; diff --git a/src/shared/image/Image.module.scss b/src/shared/image/Image.module.scss new file mode 100644 index 000000000..bea29d9ac --- /dev/null +++ b/src/shared/image/Image.module.scss @@ -0,0 +1,5 @@ +.image { + width: 100%; + height: auto; + border-radius: 8px; +} diff --git a/src/shared/image/Image.tsx b/src/shared/image/Image.tsx new file mode 100644 index 000000000..9e068027d --- /dev/null +++ b/src/shared/image/Image.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import s from './Image.module.scss'; + +type ImageProps = { + url: string; + title: string; +}; + +export const Image = ({ url, title }: ImageProps) => {title}; diff --git a/src/shared/layout/Layout.module.scss b/src/shared/layout/Layout.module.scss new file mode 100644 index 000000000..407c9e5bf --- /dev/null +++ b/src/shared/layout/Layout.module.scss @@ -0,0 +1,5 @@ +.content { + margin: 0; + padding: 20px; + background-color: #f4f4f4; +} diff --git a/src/shared/layout/Layout.tsx b/src/shared/layout/Layout.tsx new file mode 100644 index 000000000..1842dcdeb --- /dev/null +++ b/src/shared/layout/Layout.tsx @@ -0,0 +1,315 @@ +import React from 'react'; +import { Header } from '../header/Header'; +import s from './Layout.module.scss'; + +const Content = () => ( +

+

Lorem ipsum dolor sit amet.

+

Odit necessitatibus iure quas quaerat!

+

Quae vitae deserunt vero accusantium?

+

Id laborum quaerat rem laboriosam.

+

Molestias saepe assumenda optio ab!

+

Facilis nostrum natus consectetur quam.

+

Doloremque illum ducimus ut assumenda.

+

Numquam doloribus autem nemo sint.

+

Repellendus ex odio aperiam pariatur.

+

Assumenda natus ratione labore tempora.

+

Delectus, cumque. Enim, aut iste.

+

Ipsa fugit ad temporibus reprehenderit.

+

Quos beatae assumenda fuga repellat.

+

Odit odio velit ducimus repellat?

+

Placeat possimus vero alias perferendis.

+

Facilis, inventore. Ratione, delectus illum?

+

Necessitatibus nisi harum assumenda eligendi!

+

Nisi laboriosam et voluptates. Repudiandae.

+

Commodi illo dolore ipsa eos.

+

Optio incidunt veniam officiis reiciendis.

+

Illo odio sapiente eum aliquam.

+

Et aspernatur enim provident sit.

+

Totam maiores dicta magnam deserunt.

+

Doloremque amet magni tempore expedita.

+

Dolores asperiores expedita aut magnam!

+

Consectetur tempora atque perferendis odio.

+

Cum eius officiis repudiandae fuga.

+

Officiis repellat ducimus architecto culpa.

+

Ducimus odio vero corrupti error!

+

Dolore perspiciatis consequuntur culpa commodi!

+

Explicabo sapiente laudantium quae atque.

+

Hic reiciendis dignissimos vitae veritatis!

+

Provident veniam ut ad consectetur.

+

Maxime nobis voluptates deleniti consectetur!

+

Neque soluta itaque voluptatum alias!

+

Ad, ipsa quo. Nulla, laborum.

+

Inventore voluptatem quas eveniet rerum!

+

Omnis modi possimus animi voluptatum!

+

Voluptatem natus alias fugiat accusamus.

+

Itaque eos maiores corporis unde!

+

Cum repudiandae ad autem et!

+

Cum provident placeat veniam suscipit.

+

Animi, possimus. Distinctio, quos necessitatibus?

+

Consequatur dolore impedit dicta officiis!

+

Maxime eligendi voluptatum aspernatur repudiandae!

+

Maiores accusantium officia veniam odit!

+

Dicta, eius. Dolore, maxime optio?

+

Accusamus ad nihil numquam quae.

+

Atque, quidem! Perspiciatis, non unde.

+

Laboriosam eum consequuntur saepe iusto.

+

Facere aliquam sit ad omnis.

+

Consectetur voluptate doloribus nemo et!

+

Accusamus debitis quaerat beatae quas.

+

Ipsa consectetur tenetur dolorem omnis.

+

Blanditiis ad asperiores culpa at.

+

Cupiditate itaque iure doloribus hic.

+

Numquam hic incidunt voluptate fugiat?

+

Illo distinctio explicabo porro maiores.

+

Numquam ea totam officiis voluptatibus.

+

Ex recusandae est perspiciatis quos!

+

Dolores iste recusandae modi facilis?

+

Asperiores id vitae odio officiis.

+

Blanditiis numquam enim accusantium quia.

+

Delectus quas tenetur iure voluptatibus?

+

Illo aliquam inventore exercitationem quia.

+

Reprehenderit, voluptates modi! Molestias, quae!

+

Iure dicta repellat facilis in!

+

Eos fugit tempora dolor molestias.

+

Earum neque consectetur magnam exercitationem!

+

Facere repudiandae vel assumenda odit!

+

Dolore expedita officiis sed voluptatem.

+

Ratione minima odit blanditiis voluptatum!

+

Quibusdam laborum reiciendis placeat iste!

+

Architecto obcaecati eaque nihil non!

+

Dolore nihil placeat alias qui!

+

Beatae veritatis assumenda illo ipsum?

+

Blanditiis animi facilis adipisci suscipit.

+

Voluptatibus, architecto aut. Aliquam, molestias!

+

Reprehenderit illo reiciendis minima. Tempore.

+

Commodi dicta totam tenetur dolore?

+

Repellat, ratione corporis! Placeat, soluta.

+

Totam similique pariatur magnam modi.

+

Expedita exercitationem ipsum debitis velit.

+

Magni perferendis aut deserunt alias.

+

Consectetur sint aspernatur dolor quidem.

+

Incidunt porro voluptates assumenda odio!

+

Quae nemo laborum nihil nulla.

+

Error voluptas itaque dicta suscipit.

+

Rem magnam amet dignissimos consequuntur.

+

Tempore corrupti nesciunt reiciendis ratione!

+

Eum quasi fugit tempore adipisci.

+

Vel fugiat unde ipsa repellat.

+

Suscipit, vero? Voluptatum, suscipit id!

+

Eligendi itaque magnam et? Voluptatibus?

+

Aperiam laborum tempore explicabo reiciendis.

+

Cupiditate esse excepturi ab eos.

+

Vero animi soluta omnis praesentium!

+

Incidunt at laborum veritatis quis.

+

Facilis dolorum sunt dolores doloribus.

+

Ab officiis eaque iste doloribus.

+

Assumenda necessitatibus tempore fugiat rerum.

+

Nam quasi nihil minus ipsum.

+

Voluptatem temporibus possimus assumenda cum?

+

Tenetur et eos possimus fugit?

+

Consequatur dolorum at odit a.

+

Consequatur exercitationem ipsum nisi expedita.

+

Modi, neque? Nesciunt, nobis laborum.

+

Optio est tempore amet ratione?

+

Dolor repudiandae laboriosam ullam nobis!

+

Iste assumenda nostrum est suscipit?

+

Porro veniam quidem iste in.

+

Praesentium quo molestias dolorum odio.

+

Minus veniam culpa repellendus esse.

+

Provident tempore commodi nemo corporis?

+

Laudantium quibusdam sunt aliquam ab?

+

Architecto consequuntur voluptas distinctio delectus?

+

Dignissimos excepturi deleniti reprehenderit tempora.

+

Iure atque deserunt consequatur laborum?

+

Ullam, modi vero. Error, doloremque!

+

Veritatis fugiat odit libero exercitationem!

+

Sapiente placeat perferendis laudantium error!

+

Accusamus vero magnam illo molestiae?

+

Doloremque minus exercitationem error neque?

+

Quam corrupti culpa repellat adipisci?

+

Id harum fugit tempora quas?

+

Quo eveniet aperiam dignissimos cupiditate.

+

Aut quasi quidem in laborum?

+

Laudantium necessitatibus totam itaque impedit.

+

Deserunt quo porro quasi ut.

+

Repellendus doloremque nam enim laudantium.

+

Modi quae nesciunt ipsam animi!

+

Minima aut repellat ab reiciendis?

+

Doloremque eaque sequi doloribus laboriosam.

+

Ratione repudiandae natus aliquid sunt?

+

Velit ratione vel dolore maxime.

+

Temporibus, dolore. Aspernatur, voluptate nisi?

+

Eius quos sed delectus sit.

+

Ut cupiditate sequi esse ipsum.

+

Veniam voluptas exercitationem quibusdam animi.

+

Cum temporibus nobis fugiat repellat?

+

Vitae, atque sapiente! Omnis, fugit.

+

Perspiciatis consequatur optio aut sunt.

+

Nesciunt itaque eaque tenetur aliquam!

+

Fugit accusamus veniam culpa adipisci!

+

Blanditiis numquam ipsam culpa optio.

+

Molestiae eum fugiat architecto rerum!

+

Saepe fugit dolor totam corporis!

+

Quaerat aliquam vero officiis a.

+

Ab magni modi quas sit.

+

Reiciendis suscipit unde ducimus quasi.

+

Molestias laudantium facilis dicta consectetur.

+

Distinctio adipisci autem accusamus quibusdam!

+

Possimus hic eaque illum a?

+

Culpa labore reprehenderit voluptate tempore?

+

Quod, doloremque. Natus, ipsa quo.

+

Illum molestias ducimus sequi magnam.

+

Explicabo nostrum amet quia hic.

+

Vero aperiam tempora quos reiciendis.

+

Placeat fuga ea quos qui.

+

Laboriosam, dolores? Nesciunt, facere neque?

+

Praesentium voluptas animi dolor repudiandae!

+

Velit ullam suscipit consequuntur adipisci.

+

Eius commodi quisquam doloribus cupiditate!

+

Illo debitis perferendis quis iusto!

+

Aut laudantium mollitia nobis assumenda.

+

Nisi, quasi! Temporibus, ducimus quam.

+

In pariatur tenetur ducimus rem?

+

Unde dolore eos accusamus molestiae.

+

Error voluptatum ipsam sunt ex.

+

Voluptates temporibus vitae magni earum!

+

Alias ipsam tenetur placeat quae.

+

Explicabo consectetur vero quidem saepe.

+

Facilis consectetur nulla culpa veritatis?

+

Laudantium dolorem animi vel tempore!

+

Iste eius ex mollitia quis?

+

Eum error eligendi aspernatur optio!

+

Delectus suscipit maxime minus quo.

+

Ad cupiditate ducimus aspernatur aperiam.

+

Obcaecati accusamus eius natus cupiditate!

+

Voluptatem maxime voluptatum odio blanditiis?

+

Illum sapiente amet officia ipsa.

+

Sit exercitationem minima quaerat nobis!

+

Nulla nesciunt quia ea temporibus!

+

Omnis totam magnam laboriosam? Doloribus!

+

Voluptatem excepturi dolores culpa distinctio.

+

Accusamus alias architecto obcaecati repudiandae.

+

Dolores mollitia corporis laborum hic?

+

Laboriosam, beatae assumenda? Officia, vel!

+

Provident excepturi minus fugiat iusto?

+

Similique voluptas doloremque consequuntur optio?

+

Ea voluptates dolor numquam quasi.

+

Cupiditate distinctio nesciunt temporibus molestiae?

+

Laborum harum autem expedita excepturi?

+

Tenetur consequatur distinctio alias itaque.

+

Nam officiis minima modi totam.

+

Velit deleniti accusantium illum natus?

+

Exercitationem, alias? Voluptate, quis nulla!

+

Asperiores odit quasi itaque sed!

+

Incidunt nulla quae odit sequi.

+

Incidunt fugit omnis neque fugiat?

+

Ipsum esse veniam atque nam?

+

Minus sequi consectetur inventore repellat!

+

Alias corporis harum ad optio.

+

Aliquam sed mollitia magnam pariatur?

+

Ipsum sed odio sunt officiis.

+

Pariatur numquam facilis dolorum excepturi.

+

Nemo quae culpa incidunt laboriosam.

+

Fuga provident molestias a esse?

+

Pariatur, sequi quod. Debitis, quos.

+

Earum voluptates dolorum dolor rem!

+

Eveniet dolor quis aperiam accusamus?

+

Doloremque expedita id vero quisquam!

+

Consequatur facere aperiam iusto odit?

+

Nemo harum saepe ex totam!

+

Perferendis nisi sunt id voluptatum!

+

Quam omnis esse molestias doloremque.

+

Eligendi adipisci odio hic quia?

+

Quaerat libero accusamus debitis consequuntur.

+

Voluptatem commodi veritatis quos necessitatibus.

+

Excepturi sequi similique vel nihil!

+

Voluptatem vel nostrum aliquam nihil.

+

Quos voluptatum quidem sequi nulla?

+

Ipsum soluta autem sapiente odio.

+

Non aut accusantium autem impedit.

+

Hic repellendus eligendi harum deleniti.

+

Vero provident voluptatibus quae. Cumque.

+

Voluptatibus doloremque beatae in necessitatibus.

+

Aut ducimus tempora magnam consequatur.

+

Repudiandae omnis corporis obcaecati nobis?

+

Quis similique cupiditate corrupti dignissimos.

+

Aliquid aperiam non laborum repudiandae.

+

Delectus ratione error officiis blanditiis!

+

Reiciendis labore dolores dignissimos voluptatem.

+

Optio, dolores ad. Explicabo, impedit?

+

Quas explicabo autem maiores perferendis!

+

Expedita sint optio minus debitis!

+

Veritatis voluptates vitae autem deleniti?

+

Nemo, laborum iste. Nam, et?

+

Culpa excepturi porro laborum at!

+

A suscipit numquam eius ab?

+

Sapiente distinctio est sed aliquam!

+

Ea amet consectetur eveniet doloremque?

+

Alias consequuntur obcaecati ea praesentium.

+

Tenetur nostrum fugiat eum ullam?

+

Eum enim itaque animi at?

+

Incidunt reiciendis ratione inventore neque.

+

Veritatis repellendus mollitia nisi reiciendis!

+

Ipsum numquam earum ea ipsam.

+

Consequuntur officia at deleniti eos!

+

Amet facere aliquid inventore exercitationem.

+

Neque reprehenderit dolores voluptatum aperiam.

+

Officia velit vel modi earum.

+

Adipisci, quibusdam. Magnam, asperiores voluptate?

+

Nulla placeat cupiditate odit delectus.

+

Illo velit iste sapiente mollitia!

+

Totam incidunt facilis ipsum accusantium.

+

Blanditiis, ducimus! A, quidem repudiandae?

+

Enim incidunt soluta alias vel!

+

Esse accusamus eveniet nihil quis.

+

Eaque dolore repellendus nulla sit.

+

Eveniet corporis et iusto neque.

+

Dignissimos, sequi. In, soluta explicabo.

+

Dolorum tenetur error enim aperiam?

+

Enim porro harum itaque laboriosam!

+

Deleniti necessitatibus temporibus mollitia dolorum.

+

Natus, eos! Nobis, consequuntur enim!

+

Laborum ullam architecto fuga quia.

+

Inventore, molestiae. Repellendus, quaerat modi?

+

Quis hic molestiae cupiditate suscipit.

+

Dolorum modi soluta et amet.

+

Quo labore quis modi dolores?

+

Quos optio expedita harum nihil!

+

Nesciunt laborum quia id dolore?

+

Tempore ipsam eaque facere expedita!

+

Sequi assumenda quod tempore natus?

+

Cumque maiores reprehenderit inventore optio.

+

Itaque repudiandae repellat expedita ipsum!

+

Repudiandae dicta nihil optio magnam.

+

Aliquid beatae dolores harum nobis?

+

Molestias libero id asperiores fuga.

+

Atque sed at sunt voluptate!

+

Quisquam nisi sint voluptate quis.

+

Sunt asperiores quis fugit corporis.

+

Inventore odio consequatur ratione quibusdam.

+

Vel nobis tenetur facere architecto?

+

Maxime tempore laudantium ducimus odit.

+

Sit corporis veritatis omnis itaque.

+

Quod, nemo sequi. At, distinctio!

+

Aut cupiditate perspiciatis qui natus.

+

Incidunt veniam nam quae expedita!

+

Amet fugit veniam eveniet fugiat.

+

Ipsam obcaecati ea iure hic?

+

Asperiores dicta pariatur eum vitae?

+

Corrupti tempore obcaecati beatae explicabo.

+

Et eius cumque quidem minus!

+

Ipsam id adipisci voluptas soluta?

+

Asperiores earum voluptas ducimus dignissimos!

+

Aspernatur esse autem mollitia ut?

+

Expedita ex atque soluta at?

+

Laboriosam laborum quasi ipsum est.

+

+); + +export const Layout = () => ( + <> +
+ + +); diff --git a/src/shared/logo/Logo.module.scss b/src/shared/logo/Logo.module.scss new file mode 100644 index 000000000..6d0626cba --- /dev/null +++ b/src/shared/logo/Logo.module.scss @@ -0,0 +1,13 @@ +.logo { + width: 80px; + height: 50px; + background-color: #f4f4f4; + border-radius: 50%; + display: flex; + justify-content: center; + align-items: center; + color: #333; + font-family: 'Courier New', Courier, monospace; + font-size: 1.35rem; + font-weight: bold; +} diff --git a/src/shared/logo/Logo.tsx b/src/shared/logo/Logo.tsx new file mode 100644 index 000000000..5552aa778 --- /dev/null +++ b/src/shared/logo/Logo.tsx @@ -0,0 +1,4 @@ +import React from 'react'; +import s from './Logo.module.scss'; + +export const Logo = () =>
LOGO
; diff --git a/src/shared/modal-form/ModalForm.module.scss b/src/shared/modal-form/ModalForm.module.scss new file mode 100644 index 000000000..7251b0068 --- /dev/null +++ b/src/shared/modal-form/ModalForm.module.scss @@ -0,0 +1,37 @@ +.modal { + display: flex; + align-items: center; + z-index: 1; + left: 0; + top: 0; + width: 100%; + height: 500px; + overflow: auto; + background-color: rgba(0, 0, 0, 0.4); + + .modal-content { + background-color: #fefefe; + margin: 15% auto; + padding: 20px; + border: 1px solid #888; + width: 60%; + + .close { + color: #aaa; + float: right; + font-size: 28px; + font-weight: bold; + + &:hover, + &:focus { + color: black; + text-decoration: none; + cursor: pointer; + } + } + } +} + +.modal-hide { + display: none; +} diff --git a/src/shared/modal-form/ModalForm.tsx b/src/shared/modal-form/ModalForm.tsx new file mode 100644 index 000000000..fe6663efa --- /dev/null +++ b/src/shared/modal-form/ModalForm.tsx @@ -0,0 +1,19 @@ +import React, { ReactNode } from 'react'; +import cn from 'clsx'; +import s from './ModalForm.module.scss'; + +type ModalFormProps = { + visible?: boolean; + children?: ReactNode; +}; + +export const ModalForm = ({ visible = true, children }: ModalFormProps) => { + return ( +
+
+ × + {children} +
+
+ ); +}; diff --git a/src/shared/mt15/MT15.modules.scss b/src/shared/mt15/MT15.modules.scss new file mode 100644 index 000000000..712e044d8 --- /dev/null +++ b/src/shared/mt15/MT15.modules.scss @@ -0,0 +1,3 @@ +.mt15 { + margin-top: 15px; +} diff --git a/src/shared/mt15/MT15.tsx b/src/shared/mt15/MT15.tsx new file mode 100644 index 000000000..bdaf43c23 --- /dev/null +++ b/src/shared/mt15/MT15.tsx @@ -0,0 +1,8 @@ +import React, { ReactNode } from 'react'; +import s from './MT15.modules.scss'; + +type MT15Props = { + children: ReactNode; +}; + +export const MT15 = ({ children }: MT15Props) =>
{children}
; diff --git a/src/stories/AddToCart.stories.ts b/src/stories/AddToCart.stories.ts new file mode 100644 index 000000000..74d3cb5c4 --- /dev/null +++ b/src/stories/AddToCart.stories.ts @@ -0,0 +1,17 @@ +import type { Meta } from '@storybook/react'; + +import { AddToCart } from '../shared/i-shop/AddToCart'; + +const meta: Meta = { + component: AddToCart, + title: 'Интернет-магазин/В корзину', + tags: ['autodocs'], +}; + +export default meta; + +export const Test = { + args: { + quantity: 5, + }, +}; diff --git a/src/stories/Button.stories.ts b/src/stories/Button.stories.ts deleted file mode 100644 index 8e2958cdd..000000000 --- a/src/stories/Button.stories.ts +++ /dev/null @@ -1,43 +0,0 @@ -import type { Meta } from '@storybook/react'; - -import { Button } from './Button'; - -// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction -const meta: Meta = { - title: 'Example/Button', - component: Button, - tags: ['autodocs'], - argTypes: { - backgroundColor: { control: 'color' }, - }, -}; - -export default meta; - -// More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args -export const Primary = { - args: { - primary: true, - label: 'Button', - }, -}; - -export const Secondary = { - args: { - label: 'Button', - }, -}; - -export const Large = { - args: { - size: 'large', - label: 'Button', - }, -}; - -export const Small = { - args: { - size: 'small', - label: 'Button', - }, -}; diff --git a/src/stories/Button.tsx b/src/stories/Button.tsx deleted file mode 100644 index 244ff033e..000000000 --- a/src/stories/Button.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import React from 'react'; -import s from './button.module.sass'; - -interface ButtonProps { - /** - * Is this the principal call to action on the page? - */ - primary?: boolean; - /** - * What background color to use - */ - backgroundColor?: string; - /** - * How large should the button be? - */ - size?: 'small' | 'medium' | 'large'; - /** - * Button contents - */ - label: string; - /** - * Optional click handler - */ - onClick?: () => void; -} - -/** - * Primary UI component for user interaction - */ -export function Button({ primary = false, size = 'medium', backgroundColor, label, ...props }: ButtonProps) { - const mode = primary ? s.primary : s.secondary; - return ( - - ); -} diff --git a/src/stories/Header.stories.ts b/src/stories/Header.stories.ts index c74c3732a..542e67e0b 100644 --- a/src/stories/Header.stories.ts +++ b/src/stories/Header.stories.ts @@ -1,26 +1,17 @@ -import type { Meta, StoryObj } from '@storybook/react'; -import { Header } from './Header'; +import type { Meta } from '@storybook/react'; + +import { Header } from '../shared/header/Header'; const meta: Meta = { - title: 'Example/Header', component: Header, - // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/react/writing-docs/autodocs + title: 'Общее задание/Header', tags: ['autodocs'], - parameters: { - // More on how to position stories at: https://storybook.js.org/docs/react/configure/story-layout - layout: 'fullscreen', - }, }; export default meta; -type Story = StoryObj; -export const LoggedIn = { +export const WithLogo = { args: { - user: { - name: 'Jane Doe', - }, + showLogo: true, }, }; - -export const LoggedOut: Story = {}; diff --git a/src/stories/Header.tsx b/src/stories/Header.tsx deleted file mode 100644 index 555e7ce46..000000000 --- a/src/stories/Header.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import React from 'react'; - -import { Button } from './Button'; -import './header.css'; - -type User = { - name: string; -}; - -interface HeaderProps { - user?: User; - onLogin: () => void; - onLogout: () => void; - onCreateAccount: () => void; -} - -export function Header({ user, onLogin, onLogout, onCreateAccount }: HeaderProps) { - return ( -
-
-
- - - - - - - -

Acme

-
-
- {user ? ( - <> - - Welcome, {user.name}! - -
-
-
- ); -} diff --git a/src/stories/Layout.stories.ts b/src/stories/Layout.stories.ts new file mode 100644 index 000000000..3cf76fca1 --- /dev/null +++ b/src/stories/Layout.stories.ts @@ -0,0 +1,13 @@ +import type { Meta } from '@storybook/react'; + +import { Layout } from '../shared/layout/Layout'; + +const meta: Meta = { + component: Layout, + title: 'Общее задание/Layout', + tags: ['autodocs'], +}; + +export default meta; + +export const LayoutWithContent = {}; diff --git a/src/stories/Logo.stories.ts b/src/stories/Logo.stories.ts new file mode 100644 index 000000000..41f52e347 --- /dev/null +++ b/src/stories/Logo.stories.ts @@ -0,0 +1,13 @@ +import type { Meta } from '@storybook/react'; + +import { Logo } from '../shared/logo/Logo'; + +const meta: Meta = { + component: Logo, + title: 'Общее задание/Logo', + tags: ['autodocs'], +}; + +export default meta; + +export const CSSLogo = {}; diff --git a/src/stories/ModalForm.stories.tsx b/src/stories/ModalForm.stories.tsx new file mode 100644 index 000000000..e4d70d61d --- /dev/null +++ b/src/stories/ModalForm.stories.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import type { Meta } from '@storybook/react'; + +import { ModalForm } from '../shared/modal-form/ModalForm'; + +const SomeContent = () => ( +

+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Et voluptatum laudantium officiis, maiores cum sunt dolores + maxime deserunt distinctio optio ipsa illum veniam consectetur corrupti laboriosam debitis corporis voluptates + cumque. +

+); + +const meta: Meta = { + component: ModalForm, + title: 'Общее задание/Модальное окно', + tags: ['autodocs'], + argTypes: { + children: { + control: { type: 'boolean' }, + mapping: { false: '', true: }, + }, + }, + parameters: { + docs: { + story: { + height: '500px', + }, + }, + }, +}; + +export default meta; + +export const NotVisible = { + args: { + visible: false, + }, +}; + +export const Blank = { + args: { + visible: true, + }, +}; + +export const WithContent = { + args: { + visible: true, + children: , + }, +}; diff --git a/src/stories/OperationDetail.stories.tsx b/src/stories/OperationDetail.stories.tsx new file mode 100644 index 000000000..d9efd0dff --- /dev/null +++ b/src/stories/OperationDetail.stories.tsx @@ -0,0 +1,22 @@ +import type { Meta } from '@storybook/react'; + +import { OperationDetail } from '../shared/icome-expenses-accounting/OperationDetail'; + +const meta: Meta = { + component: OperationDetail, + title: 'Учет доходов-расходов/Полная операция', + tags: ['autodocs'], +}; + +export default meta; + +export const Test = { + args: { + amount: 2999.99, + category: 'оплата', + title: 'Подписка', + description: + 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Minus omnis tempore cupiditate magni ad porro nihil consectetur a voluptas, rerum error, maiores rem, ut adipisci sint? Esse excepturi at non?', + date: new Date().toLocaleDateString(), + }, +}; diff --git a/src/stories/OperationSummary.stories.tsx b/src/stories/OperationSummary.stories.tsx new file mode 100644 index 000000000..4bf0b3ddc --- /dev/null +++ b/src/stories/OperationSummary.stories.tsx @@ -0,0 +1,21 @@ +import type { Meta } from '@storybook/react'; + +import { OperationSummary } from '../shared/icome-expenses-accounting/OperationSummary'; + +const meta: Meta = { + component: OperationSummary, + title: 'Учет доходов-расходов/Краткая операция', + tags: ['autodocs'], +}; + +export default meta; + +export const Test = { + args: { + amount: 2999.99, + category: 'оплата', + title: 'Подписка', + shortDescription: + 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Minus omnis tempore cupiditate magni ad porro nihil consectetur a voluptas, rerum error, maiores rem, ut adipisci sint? Esse excepturi at non?', + }, +}; diff --git a/src/stories/Page.stories.ts b/src/stories/Page.stories.ts deleted file mode 100644 index 905187f96..000000000 --- a/src/stories/Page.stories.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react'; -import { within, userEvent } from '@storybook/testing-library'; - -import { Page } from './Page'; - -const meta: Meta = { - title: 'Example/Page', - component: Page, - parameters: { - // More on how to position stories at: https://storybook.js.org/docs/react/configure/story-layout - layout: 'fullscreen', - }, -}; - -export default meta; -type Story = StoryObj; - -export const LoggedOut: Story = {}; - -// More on interaction testing: https://storybook.js.org/docs/react/writing-tests/interaction-testing -export const LoggedIn: Story = { - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - const loginButton = await canvas.getByRole('button', { - name: /Log in/i, - }); - await userEvent.click(loginButton); - }, -}; diff --git a/src/stories/Page.tsx b/src/stories/Page.tsx deleted file mode 100644 index 7b01ba5ce..000000000 --- a/src/stories/Page.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import React from 'react'; -import { Header } from './Header'; -import './page.css'; - -type User = { - name: string; -}; - -export const Page: React.FC = () => { - const [user, setUser] = React.useState(); - - return ( -
-
setUser({ name: 'Jane Doe' })} - onLogout={() => setUser(undefined)} - onCreateAccount={() => setUser({ name: 'Jane Doe' })} - /> - -
-

Pages in Storybook

-

- We recommend building UIs with a{' '} - - component-driven - {' '} - process starting with atomic components and ending with pages. -

-

- Render pages with mock data. This makes it easy to build and review page states without needing to navigate to - them in your app. Here are some handy patterns for managing page data in Storybook: -

-
    -
  • - Use a higher-level connected component. Storybook helps you compose such data from the args of child - component stories -
  • -
  • - Assemble data in the page component from your services. You can mock these services out using Storybook. -
  • -
-

- Get a guided tutorial on component-driven development at{' '} - - Storybook tutorials - - . Read more in the{' '} - - docs - - . -

-
- Tip Adjust the width of the canvas with the{' '} - - - - - - Viewports addon in the toolbar -
-
-
- ); -}; diff --git a/src/stories/ProductCart.stories.ts b/src/stories/ProductCart.stories.ts new file mode 100644 index 000000000..2e68091a4 --- /dev/null +++ b/src/stories/ProductCart.stories.ts @@ -0,0 +1,20 @@ +import type { Meta } from '@storybook/react'; + +import { ProductCart } from '../shared/i-shop/ProductCart'; + +const meta: Meta = { + component: ProductCart, + title: 'Интернет-магазин/Товар для корзины', + tags: ['autodocs'], +}; + +export default meta; + +export const Test = { + args: { + product: { + title: 'TEST Title', + price: 999.99, + }, + }, +}; diff --git a/src/stories/ProductDetail.stories.ts b/src/stories/ProductDetail.stories.ts new file mode 100644 index 000000000..9fb37cf0b --- /dev/null +++ b/src/stories/ProductDetail.stories.ts @@ -0,0 +1,22 @@ +import type { Meta } from '@storybook/react'; + +import { ProductDetail } from '../shared/i-shop/ProductDetail'; + +const meta: Meta = { + component: ProductDetail, + title: 'Интернет-магазин/Полное отображение', + tags: ['autodocs'], +}; + +export default meta; + +export const Test = { + args: { + title: 'TEST Title', + price: 999.99, + category: "овощи", + images: ['https://placehold.co/600x400', 'https://placehold.co/600x400'], + description: + 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Minus omnis tempore cupiditate magni ad porro nihil consectetur a voluptas, rerum error, maiores rem, ut adipisci sint? Esse excepturi at non?', + }, +}; diff --git a/src/stories/ProductSummary.stories.ts b/src/stories/ProductSummary.stories.ts new file mode 100644 index 000000000..19b9c274e --- /dev/null +++ b/src/stories/ProductSummary.stories.ts @@ -0,0 +1,21 @@ +import type { Meta } from '@storybook/react'; + +import { ProductSummary } from '../shared/i-shop/ProductSummary'; + +const meta: Meta = { + component: ProductSummary, + title: 'Интернет-магазин/Краткое отображение', + tags: ['autodocs'], +}; + +export default meta; + +export const Test = { + args: { + title: 'TEST Title', + price: 999.99, + image: 'https://placehold.co/600x400', + shortDescription: + 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Minus omnis tempore cupiditate magni ad porro nihil consectetur a voluptas, rerum error, maiores rem, ut adipisci sint? Esse excepturi at non?', + }, +}; diff --git a/src/stories/button.module.sass b/src/stories/button.module.sass deleted file mode 100644 index afb77db88..000000000 --- a/src/stories/button.module.sass +++ /dev/null @@ -1,30 +0,0 @@ -.button - font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif - font-weight: 700 - border: 0 - border-radius: 3em - cursor: pointer - display: inline-block - line-height: 1 - - &.primary - color: white - background-color: #1ea7fd - - &.secondary - color: #333 - background-color: transparent - box-shadow: rgba(0, 0, 0, 0.15) 0px 0px 0px 1px inset - - &.small - font-size: 12px - padding: 10px 16px - - &.medium - font-size: 14px - padding: 11px 20px - - &.large - font-size: 16px - padding: 12px 24px - diff --git a/src/stories/header.css b/src/stories/header.css deleted file mode 100644 index e30766072..000000000 --- a/src/stories/header.css +++ /dev/null @@ -1,32 +0,0 @@ -.storybook-header { - font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; - border-bottom: 1px solid rgba(0, 0, 0, 0.1); - padding: 15px 20px; - display: flex; - align-items: center; - justify-content: space-between; -} - -.storybook-header svg { - display: inline-block; - vertical-align: top; -} - -.storybook-header h1 { - font-weight: 700; - font-size: 20px; - line-height: 1; - margin: 6px 0 6px 10px; - display: inline-block; - vertical-align: top; -} - -.storybook-header button + button { - margin-left: 10px; -} - -.storybook-header .welcome { - color: #333; - font-size: 14px; - margin-right: 10px; -} diff --git a/src/stories/page.css b/src/stories/page.css deleted file mode 100644 index 139f6885a..000000000 --- a/src/stories/page.css +++ /dev/null @@ -1,69 +0,0 @@ -.storybook-page { - font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; - font-size: 14px; - line-height: 24px; - padding: 48px 20px; - margin: 0 auto; - max-width: 600px; - color: #333; -} - -.storybook-page h2 { - font-weight: 700; - font-size: 32px; - line-height: 1; - margin: 0 0 4px; - display: inline-block; - vertical-align: top; -} - -.storybook-page p { - margin: 1em 0; -} - -.storybook-page a { - text-decoration: none; - color: #1ea7fd; -} - -.storybook-page ul { - padding-left: 30px; - margin: 1em 0; -} - -.storybook-page li { - margin-bottom: 8px; -} - -.storybook-page .tip { - display: inline-block; - border-radius: 1em; - font-size: 11px; - line-height: 12px; - font-weight: 700; - background: #e7fdd8; - color: #66bf3c; - padding: 4px 12px; - margin-right: 10px; - vertical-align: top; -} - -.storybook-page .tip-wrapper { - font-size: 13px; - line-height: 20px; - margin-top: 40px; - margin-bottom: 40px; -} - -.storybook-page .tip-wrapper svg { - display: inline-block; - height: 12px; - width: 12px; - margin-right: 4px; - vertical-align: top; - margin-top: 3px; -} - -.storybook-page .tip-wrapper svg path { - fill: #1ea7fd; -} From 83527a39496a70ce75c17b787c85ddb34b642375 Mon Sep 17 00:00:00 2001 From: Igor Aralov Date: Tue, 26 Nov 2024 18:53:48 +0300 Subject: [PATCH 10/27] Fix homework 2,3 --- src/shared/header/Header.tsx | 2 +- src/shared/i-shop/ProductSummary.tsx | 19 -------------- .../{ => add-to-cart}/AddToCart.module.scss | 0 .../i-shop/{ => add-to-cart}/AddToCart.tsx | 2 +- .../ProductCart.module.scss | 0 .../i-shop/{ => product-cart}/ProductCart.tsx | 9 ++++--- .../ProductContainer.module.scss | 0 .../ProductContainer.tsx | 7 ++++-- .../ProductDetail.module.scss | 0 .../{ => product-detail}/ProductDetail.tsx | 19 ++++++++------ .../i-shop/product-summary/ProductSummary.tsx | 23 +++++++++++++++++ src/shared/i-shop/product.types.ts | 25 ------------------- .../i-shop/{ => product}/Product.module.scss | 0 src/shared/i-shop/{ => product}/Product.tsx | 6 ++++- .../OperationSummary.tsx | 12 --------- .../OperationContainer.module.scss | 0 .../OperationContainer.tsx | 7 ++++-- .../OperationDetail.module.scss | 0 .../OperationDetail.tsx | 13 +++++----- .../operation-summary/OperationSummary.tsx | 15 +++++++++++ .../operation.types.ts | 17 ------------- .../{ => operation}/Operation.module.scss | 0 .../{ => operation}/Operation.tsx | 7 +++++- src/shared/layout/Layout.tsx | 2 +- src/shared/modal-form/ModalForm.tsx | 4 +-- src/stories/AddToCart.stories.ts | 2 +- src/stories/OperationDetail.stories.tsx | 2 +- src/stories/OperationSummary.stories.tsx | 2 +- src/stories/ProductCart.stories.ts | 2 +- src/stories/ProductDetail.stories.ts | 4 +-- src/stories/ProductSummary.stories.ts | 2 +- 31 files changed, 96 insertions(+), 107 deletions(-) delete mode 100644 src/shared/i-shop/ProductSummary.tsx rename src/shared/i-shop/{ => add-to-cart}/AddToCart.module.scss (100%) rename src/shared/i-shop/{ => add-to-cart}/AddToCart.tsx (92%) rename src/shared/i-shop/{ => product-cart}/ProductCart.module.scss (100%) rename src/shared/i-shop/{ => product-cart}/ProductCart.tsx (65%) rename src/shared/i-shop/{ => product-container}/ProductContainer.module.scss (100%) rename src/shared/i-shop/{ => product-container}/ProductContainer.tsx (64%) rename src/shared/i-shop/{ => product-detail}/ProductDetail.module.scss (100%) rename src/shared/i-shop/{ => product-detail}/ProductDetail.tsx (56%) create mode 100644 src/shared/i-shop/product-summary/ProductSummary.tsx delete mode 100644 src/shared/i-shop/product.types.ts rename src/shared/i-shop/{ => product}/Product.module.scss (100%) rename src/shared/i-shop/{ => product}/Product.tsx (79%) delete mode 100644 src/shared/icome-expenses-accounting/OperationSummary.tsx rename src/shared/icome-expenses-accounting/{ => operation-container}/OperationContainer.module.scss (100%) rename src/shared/icome-expenses-accounting/{ => operation-container}/OperationContainer.tsx (63%) rename src/shared/icome-expenses-accounting/{ => operation-detail}/OperationDetail.module.scss (100%) rename src/shared/icome-expenses-accounting/{ => operation-detail}/OperationDetail.tsx (54%) create mode 100644 src/shared/icome-expenses-accounting/operation-summary/OperationSummary.tsx delete mode 100644 src/shared/icome-expenses-accounting/operation.types.ts rename src/shared/icome-expenses-accounting/{ => operation}/Operation.module.scss (100%) rename src/shared/icome-expenses-accounting/{ => operation}/Operation.tsx (76%) diff --git a/src/shared/header/Header.tsx b/src/shared/header/Header.tsx index 3fb6135f7..456d1021a 100644 --- a/src/shared/header/Header.tsx +++ b/src/shared/header/Header.tsx @@ -3,7 +3,7 @@ import { Logo } from '../logo/Logo'; import s from './Header.module.scss'; type HeaderProps = { - showLogo?: boolean; + showLogo: boolean; }; export const Header = ({ showLogo = true }: HeaderProps) =>
{showLogo && }
; diff --git a/src/shared/i-shop/ProductSummary.tsx b/src/shared/i-shop/ProductSummary.tsx deleted file mode 100644 index f46c029e0..000000000 --- a/src/shared/i-shop/ProductSummary.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from 'react'; -import { ProductSummary as ProductSummaryProps } from './product.types'; -import { ProductContainer } from './ProductContainer'; -import { Product } from './Product'; -import { Description } from '../description/Description'; -import { AddToCart } from './AddToCart'; -import { Image } from '../image/Image'; -import { MT15 } from '../mt15/MT15'; - -export const ProductSummary = (data: ProductSummaryProps) => ( - - - - - - - - -); diff --git a/src/shared/i-shop/AddToCart.module.scss b/src/shared/i-shop/add-to-cart/AddToCart.module.scss similarity index 100% rename from src/shared/i-shop/AddToCart.module.scss rename to src/shared/i-shop/add-to-cart/AddToCart.module.scss diff --git a/src/shared/i-shop/AddToCart.tsx b/src/shared/i-shop/add-to-cart/AddToCart.tsx similarity index 92% rename from src/shared/i-shop/AddToCart.tsx rename to src/shared/i-shop/add-to-cart/AddToCart.tsx index a5cddbce5..aa4e60b52 100644 --- a/src/shared/i-shop/AddToCart.tsx +++ b/src/shared/i-shop/add-to-cart/AddToCart.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Button } from '../button/Button'; +import { Button } from '../../button/Button'; import s from './AddToCart.module.scss'; type AddToCartProps = { diff --git a/src/shared/i-shop/ProductCart.module.scss b/src/shared/i-shop/product-cart/ProductCart.module.scss similarity index 100% rename from src/shared/i-shop/ProductCart.module.scss rename to src/shared/i-shop/product-cart/ProductCart.module.scss diff --git a/src/shared/i-shop/ProductCart.tsx b/src/shared/i-shop/product-cart/ProductCart.tsx similarity index 65% rename from src/shared/i-shop/ProductCart.tsx rename to src/shared/i-shop/product-cart/ProductCart.tsx index e2e533655..5e7543def 100644 --- a/src/shared/i-shop/ProductCart.tsx +++ b/src/shared/i-shop/product-cart/ProductCart.tsx @@ -1,11 +1,14 @@ import React from 'react'; -import { ProductCart as ProductCartProps } from './product.types'; -import { Product } from './Product'; -import { Button } from '../button/Button'; +import { Button } from '../../button/Button'; +import { Product, ProductProps } from '../product/Product'; import s from './ProductCart.module.scss'; const DeleteButton = () => ; +type ProductCartProps = { + product: ProductProps; +}; + export const ProductCart = ({ product }: ProductCartProps) => (
diff --git a/src/shared/i-shop/ProductContainer.module.scss b/src/shared/i-shop/product-container/ProductContainer.module.scss similarity index 100% rename from src/shared/i-shop/ProductContainer.module.scss rename to src/shared/i-shop/product-container/ProductContainer.module.scss diff --git a/src/shared/i-shop/ProductContainer.tsx b/src/shared/i-shop/product-container/ProductContainer.tsx similarity index 64% rename from src/shared/i-shop/ProductContainer.tsx rename to src/shared/i-shop/product-container/ProductContainer.tsx index b519aad14..74fabe7d8 100644 --- a/src/shared/i-shop/ProductContainer.tsx +++ b/src/shared/i-shop/product-container/ProductContainer.tsx @@ -1,7 +1,10 @@ -import React from 'react'; -import { ProductContainer as ProductContainerProps } from './product.types'; +import React, { ReactNode } from 'react'; import s from './ProductContainer.module.scss'; +type ProductContainerProps = { + children: ReactNode; +}; + export const ProductContainer = ({ children }: ProductContainerProps) => (
{children}
); diff --git a/src/shared/i-shop/ProductDetail.module.scss b/src/shared/i-shop/product-detail/ProductDetail.module.scss similarity index 100% rename from src/shared/i-shop/ProductDetail.module.scss rename to src/shared/i-shop/product-detail/ProductDetail.module.scss diff --git a/src/shared/i-shop/ProductDetail.tsx b/src/shared/i-shop/product-detail/ProductDetail.tsx similarity index 56% rename from src/shared/i-shop/ProductDetail.tsx rename to src/shared/i-shop/product-detail/ProductDetail.tsx index 8be16c184..42b9bca7b 100644 --- a/src/shared/i-shop/ProductDetail.tsx +++ b/src/shared/i-shop/product-detail/ProductDetail.tsx @@ -1,13 +1,18 @@ import React from 'react'; -import { ProductDetail as ProductDetailProps } from './product.types'; -import { ProductContainer } from './ProductContainer'; -import { Product } from './Product'; -import { Description } from '../description/Description'; -import { AddToCart } from './AddToCart'; -import { Image } from '../image/Image'; -import { MT15 } from '../mt15/MT15'; +import { Description } from '../../description/Description'; +import { AddToCart } from '../add-to-cart/AddToCart'; +import { Image } from '../../image/Image'; +import { MT15 } from '../../mt15/MT15'; +import { ProductContainer } from '../product-container/ProductContainer'; +import { Product, ProductProps } from '../product/Product'; import s from './ProductDetail.module.scss'; +type ProductDetailProps = ProductProps & { + images: string[]; + category: string; + description: string; +}; + export const ProductDetail = (data: ProductDetailProps) => (
{data.category}
diff --git a/src/shared/i-shop/product-summary/ProductSummary.tsx b/src/shared/i-shop/product-summary/ProductSummary.tsx new file mode 100644 index 000000000..b00f02345 --- /dev/null +++ b/src/shared/i-shop/product-summary/ProductSummary.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { Image } from '../../image/Image'; +import { MT15 } from '../../mt15/MT15'; +import { Description } from '../../description/Description'; +import { ProductContainer } from '../product-container/ProductContainer'; +import { Product, ProductProps } from '../product/Product'; +import { AddToCart } from '../add-to-cart/AddToCart'; + +type ProductSummaryProps = ProductProps & { + image: string; + shortDescription: string; +}; + +export const ProductSummary = (data: ProductSummaryProps) => ( + + + + + + + + +); diff --git a/src/shared/i-shop/product.types.ts b/src/shared/i-shop/product.types.ts deleted file mode 100644 index 5d2854dfb..000000000 --- a/src/shared/i-shop/product.types.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { ReactNode } from 'react'; - -export type Product = { - title: string; - price: number; -}; - -export type ProductContainer = { - children: ReactNode; -}; - -export type ProductSummary = Product & { - image: string; - shortDescription: string; -}; - -export type ProductDetail = Product & { - images: string[]; - category: string; - description: string; -}; - -export type ProductCart = { - product: Product; -}; diff --git a/src/shared/i-shop/Product.module.scss b/src/shared/i-shop/product/Product.module.scss similarity index 100% rename from src/shared/i-shop/Product.module.scss rename to src/shared/i-shop/product/Product.module.scss diff --git a/src/shared/i-shop/Product.tsx b/src/shared/i-shop/product/Product.tsx similarity index 79% rename from src/shared/i-shop/Product.tsx rename to src/shared/i-shop/product/Product.tsx index 0c333a6c9..17cd02a32 100644 --- a/src/shared/i-shop/Product.tsx +++ b/src/shared/i-shop/product/Product.tsx @@ -1,7 +1,11 @@ import React from 'react'; -import { Product as ProductProps } from './product.types'; import s from './Product.module.scss'; +export type ProductProps = { + title: string; + price: number; +}; + export const Product = (data: ProductProps) => ( <>

{data.title}

diff --git a/src/shared/icome-expenses-accounting/OperationSummary.tsx b/src/shared/icome-expenses-accounting/OperationSummary.tsx deleted file mode 100644 index 776471604..000000000 --- a/src/shared/icome-expenses-accounting/OperationSummary.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react'; -import { OperationSummary as OperationSummaryProps } from './operation.types'; -import { OperationContainer } from './OperationContainer'; -import { Operation } from './Operation'; -import { Description } from '../description/Description'; - -export const OperationSummary = (data: OperationSummaryProps) => ( - - - - -); diff --git a/src/shared/icome-expenses-accounting/OperationContainer.module.scss b/src/shared/icome-expenses-accounting/operation-container/OperationContainer.module.scss similarity index 100% rename from src/shared/icome-expenses-accounting/OperationContainer.module.scss rename to src/shared/icome-expenses-accounting/operation-container/OperationContainer.module.scss diff --git a/src/shared/icome-expenses-accounting/OperationContainer.tsx b/src/shared/icome-expenses-accounting/operation-container/OperationContainer.tsx similarity index 63% rename from src/shared/icome-expenses-accounting/OperationContainer.tsx rename to src/shared/icome-expenses-accounting/operation-container/OperationContainer.tsx index dfefb1769..0cb6356bb 100644 --- a/src/shared/icome-expenses-accounting/OperationContainer.tsx +++ b/src/shared/icome-expenses-accounting/operation-container/OperationContainer.tsx @@ -1,7 +1,10 @@ -import React from 'react'; -import { OperationContainer as OperationContainerProps } from './operation.types'; +import React, { ReactNode } from 'react'; import s from './OperationContainer.module.scss'; +type OperationContainerProps = { + children: ReactNode; +}; + export const OperationContainer = ({ children }: OperationContainerProps) => (
{children}
); diff --git a/src/shared/icome-expenses-accounting/OperationDetail.module.scss b/src/shared/icome-expenses-accounting/operation-detail/OperationDetail.module.scss similarity index 100% rename from src/shared/icome-expenses-accounting/OperationDetail.module.scss rename to src/shared/icome-expenses-accounting/operation-detail/OperationDetail.module.scss diff --git a/src/shared/icome-expenses-accounting/OperationDetail.tsx b/src/shared/icome-expenses-accounting/operation-detail/OperationDetail.tsx similarity index 54% rename from src/shared/icome-expenses-accounting/OperationDetail.tsx rename to src/shared/icome-expenses-accounting/operation-detail/OperationDetail.tsx index adb57a05c..527a96bfe 100644 --- a/src/shared/icome-expenses-accounting/OperationDetail.tsx +++ b/src/shared/icome-expenses-accounting/operation-detail/OperationDetail.tsx @@ -1,10 +1,9 @@ import React from 'react'; -import { OperationDetail as OperationDetailProps } from './operation.types'; -import { OperationContainer } from './OperationContainer'; -import { Operation } from './Operation'; -import { Description } from '../description/Description'; -import { Button } from '../button/Button'; -import { MT15 } from '../mt15/MT15'; +import { Description } from '../../description/Description'; +import { Button } from '../../button/Button'; +import { MT15 } from '../../mt15/MT15'; +import { OperationContainer } from '../operation-container/OperationContainer'; +import { Operation, OperationProps } from '../operation/Operation'; import s from './OperationDetail.module.scss'; const EditButton = () => ( @@ -13,6 +12,8 @@ const EditButton = () => ( ); +type OperationDetailProps = OperationProps & { description: string; date: string }; + export const OperationDetail = (data: OperationDetailProps) => ( diff --git a/src/shared/icome-expenses-accounting/operation-summary/OperationSummary.tsx b/src/shared/icome-expenses-accounting/operation-summary/OperationSummary.tsx new file mode 100644 index 000000000..a464f6513 --- /dev/null +++ b/src/shared/icome-expenses-accounting/operation-summary/OperationSummary.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { Description } from '../../description/Description'; +import { OperationContainer } from '../operation-container/OperationContainer'; +import { Operation, OperationProps } from '../operation/Operation'; + +type OperationSummaryProps = OperationProps & { + shortDescription: string; +}; + +export const OperationSummary = (data: OperationSummaryProps) => ( + + + + +); diff --git a/src/shared/icome-expenses-accounting/operation.types.ts b/src/shared/icome-expenses-accounting/operation.types.ts deleted file mode 100644 index d14b64746..000000000 --- a/src/shared/icome-expenses-accounting/operation.types.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { ReactNode } from 'react'; - -export type OperationContainer = { - children: ReactNode; -}; - -export type Operation = { - amount: number; - category: string; - title: string; -}; - -export type OperationSummary = Operation & { - shortDescription: string; -}; - -export type OperationDetail = Operation & { description: string; date: string }; diff --git a/src/shared/icome-expenses-accounting/Operation.module.scss b/src/shared/icome-expenses-accounting/operation/Operation.module.scss similarity index 100% rename from src/shared/icome-expenses-accounting/Operation.module.scss rename to src/shared/icome-expenses-accounting/operation/Operation.module.scss diff --git a/src/shared/icome-expenses-accounting/Operation.tsx b/src/shared/icome-expenses-accounting/operation/Operation.tsx similarity index 76% rename from src/shared/icome-expenses-accounting/Operation.tsx rename to src/shared/icome-expenses-accounting/operation/Operation.tsx index 3d1b7073e..522fe000e 100644 --- a/src/shared/icome-expenses-accounting/Operation.tsx +++ b/src/shared/icome-expenses-accounting/operation/Operation.tsx @@ -1,7 +1,12 @@ import React from 'react'; -import { Operation as OperationProps } from './operation.types'; import s from './Operation.module.scss'; +export type OperationProps = { + amount: number; + category: string; + title: string; +}; + export const Operation = (data: OperationProps) => ( <>
{data.amount}
diff --git a/src/shared/layout/Layout.tsx b/src/shared/layout/Layout.tsx index 1842dcdeb..9af8a9b13 100644 --- a/src/shared/layout/Layout.tsx +++ b/src/shared/layout/Layout.tsx @@ -309,7 +309,7 @@ const Content = () => ( export const Layout = () => ( <> -
+
); diff --git a/src/shared/modal-form/ModalForm.tsx b/src/shared/modal-form/ModalForm.tsx index fe6663efa..ee90553df 100644 --- a/src/shared/modal-form/ModalForm.tsx +++ b/src/shared/modal-form/ModalForm.tsx @@ -3,8 +3,8 @@ import cn from 'clsx'; import s from './ModalForm.module.scss'; type ModalFormProps = { - visible?: boolean; - children?: ReactNode; + visible: boolean; + children: ReactNode; }; export const ModalForm = ({ visible = true, children }: ModalFormProps) => { diff --git a/src/stories/AddToCart.stories.ts b/src/stories/AddToCart.stories.ts index 74d3cb5c4..17c30a2db 100644 --- a/src/stories/AddToCart.stories.ts +++ b/src/stories/AddToCart.stories.ts @@ -1,6 +1,6 @@ import type { Meta } from '@storybook/react'; -import { AddToCart } from '../shared/i-shop/AddToCart'; +import { AddToCart } from '../shared/i-shop/add-to-cart/AddToCart'; const meta: Meta = { component: AddToCart, diff --git a/src/stories/OperationDetail.stories.tsx b/src/stories/OperationDetail.stories.tsx index d9efd0dff..b2ade689a 100644 --- a/src/stories/OperationDetail.stories.tsx +++ b/src/stories/OperationDetail.stories.tsx @@ -1,6 +1,6 @@ import type { Meta } from '@storybook/react'; -import { OperationDetail } from '../shared/icome-expenses-accounting/OperationDetail'; +import { OperationDetail } from '../shared/icome-expenses-accounting/operation-detail/OperationDetail'; const meta: Meta = { component: OperationDetail, diff --git a/src/stories/OperationSummary.stories.tsx b/src/stories/OperationSummary.stories.tsx index 4bf0b3ddc..d3bd83b18 100644 --- a/src/stories/OperationSummary.stories.tsx +++ b/src/stories/OperationSummary.stories.tsx @@ -1,6 +1,6 @@ import type { Meta } from '@storybook/react'; -import { OperationSummary } from '../shared/icome-expenses-accounting/OperationSummary'; +import { OperationSummary } from '../shared/icome-expenses-accounting/operation-summary/OperationSummary'; const meta: Meta = { component: OperationSummary, diff --git a/src/stories/ProductCart.stories.ts b/src/stories/ProductCart.stories.ts index 2e68091a4..45db0aa4e 100644 --- a/src/stories/ProductCart.stories.ts +++ b/src/stories/ProductCart.stories.ts @@ -1,6 +1,6 @@ import type { Meta } from '@storybook/react'; -import { ProductCart } from '../shared/i-shop/ProductCart'; +import { ProductCart } from '../shared/i-shop/product-cart/ProductCart'; const meta: Meta = { component: ProductCart, diff --git a/src/stories/ProductDetail.stories.ts b/src/stories/ProductDetail.stories.ts index 9fb37cf0b..64c5427b6 100644 --- a/src/stories/ProductDetail.stories.ts +++ b/src/stories/ProductDetail.stories.ts @@ -1,6 +1,6 @@ import type { Meta } from '@storybook/react'; -import { ProductDetail } from '../shared/i-shop/ProductDetail'; +import { ProductDetail } from '../shared/i-shop/product-detail/ProductDetail'; const meta: Meta = { component: ProductDetail, @@ -14,7 +14,7 @@ export const Test = { args: { title: 'TEST Title', price: 999.99, - category: "овощи", + category: 'овощи', images: ['https://placehold.co/600x400', 'https://placehold.co/600x400'], description: 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Minus omnis tempore cupiditate magni ad porro nihil consectetur a voluptas, rerum error, maiores rem, ut adipisci sint? Esse excepturi at non?', diff --git a/src/stories/ProductSummary.stories.ts b/src/stories/ProductSummary.stories.ts index 19b9c274e..a291cf844 100644 --- a/src/stories/ProductSummary.stories.ts +++ b/src/stories/ProductSummary.stories.ts @@ -1,6 +1,6 @@ import type { Meta } from '@storybook/react'; -import { ProductSummary } from '../shared/i-shop/ProductSummary'; +import { ProductSummary } from '../shared/i-shop/product-summary/ProductSummary'; const meta: Meta = { component: ProductSummary, From cbc9244f1109e13accbc315e70ccd76680b325f4 Mon Sep 17 00:00:00 2001 From: Igor Aralov Date: Wed, 27 Nov 2024 18:38:27 +0300 Subject: [PATCH 11/27] Fix issues identified by the linter --- src/homeworks/ts1/1_base.ts | 8 ++++---- src/shared/description/Description.tsx | 4 ++-- src/shared/i-shop/product-detail/ProductDetail.tsx | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/homeworks/ts1/1_base.ts b/src/homeworks/ts1/1_base.ts index 60d9ce278..6dd43097c 100644 --- a/src/homeworks/ts1/1_base.ts +++ b/src/homeworks/ts1/1_base.ts @@ -10,12 +10,12 @@ export const removeFirstZeros = (value: string): string => value.replace(/^(-)?[ export const getBeautifulNumber = (value: number, separator = ' '): string => value?.toString().replace(/\B(?=(\d{3})+(?!\d))/g, separator); -export const round = (value: number, accuracy: number = 2): number => { +export const round = (value: number, accuracy = 2): number => { const d = 10 ** accuracy; return Math.round(value * d) / d; }; -const transformRegexp: RegExp = +const transformRegexp = /(matrix\(-?\d+(\.\d+)?, -?\d+(\.\d+)?, -?\d+(\.\d+)?, -?\d+(\.\d+)?, )(-?\d+(\.\d+)?), (-?\d+(\.\d+)?)\)/; type TransformedRegexp = { @@ -42,8 +42,8 @@ type BlackOrWhite = 'black' | 'white'; export const getContrastType = (contrastValue: number): BlackOrWhite => (contrastValue > 125 ? 'black' : 'white'); -export const shortColorRegExp: RegExp = /^#[0-9a-f]{3}$/i; -export const longColorRegExp: RegExp = /^#[0-9a-f]{6}$/i; +export const shortColorRegExp = /^#[0-9a-f]{3}$/i; +export const longColorRegExp = /^#[0-9a-f]{6}$/i; export const checkColor = (color: string): void | never => { if (!longColorRegExp.test(color) && !shortColorRegExp.test(color)) throw new Error(`invalid hex color: ${color}`); diff --git a/src/shared/description/Description.tsx b/src/shared/description/Description.tsx index 9e0b570bb..2ed14ecaa 100644 --- a/src/shared/description/Description.tsx +++ b/src/shared/description/Description.tsx @@ -2,11 +2,11 @@ import React from 'react'; import cn from 'clsx'; import s from './Description.module.scss'; -type Description = { +type DescriptionProps = { description: string; isShort?: boolean; }; -export const Description = ({ description, isShort = false }: Description) => ( +export const Description = ({ description, isShort = false }: DescriptionProps) => (
{description}
); diff --git a/src/shared/i-shop/product-detail/ProductDetail.tsx b/src/shared/i-shop/product-detail/ProductDetail.tsx index 42b9bca7b..09aadb4fd 100644 --- a/src/shared/i-shop/product-detail/ProductDetail.tsx +++ b/src/shared/i-shop/product-detail/ProductDetail.tsx @@ -18,7 +18,7 @@ export const ProductDetail = (data: ProductDetailProps) => (
{data.category}
{data.images.map((image, index) => ( - + ))}
From 72a7b94a040f8037f61d0b3c4346da9db2407911 Mon Sep 17 00:00:00 2001 From: Igor Aralov Date: Thu, 28 Nov 2024 21:18:06 +0300 Subject: [PATCH 12/27] Add StatefullModalForm --- src/shared/button/Button.tsx | 9 ++++--- src/shared/modal-form/ModalForm.module.scss | 1 + src/shared/modal-form/ModalForm.tsx | 9 ++++--- .../StatefullModalForm.module.scss | 10 ++++++++ .../StatefullModalForm.tsx | 24 +++++++++++++++++++ src/shared/text-input/TextInput.module.scss | 12 ++++++++++ src/shared/text-input/TextInput.tsx | 11 +++++++++ src/stories/InputToModal.stories.ts | 20 ++++++++++++++++ 8 files changed, 90 insertions(+), 6 deletions(-) create mode 100644 src/shared/statefull-modal-form/StatefullModalForm.module.scss create mode 100644 src/shared/statefull-modal-form/StatefullModalForm.tsx create mode 100644 src/shared/text-input/TextInput.module.scss create mode 100644 src/shared/text-input/TextInput.tsx create mode 100644 src/stories/InputToModal.stories.ts diff --git a/src/shared/button/Button.tsx b/src/shared/button/Button.tsx index fb8c9ef44..41bd4eef8 100644 --- a/src/shared/button/Button.tsx +++ b/src/shared/button/Button.tsx @@ -1,12 +1,15 @@ -import React, { ReactNode } from 'react'; +import React, { ReactNode, MouseEvent } from 'react'; import cn from 'clsx'; import s from './Button.modules.scss'; type ButtonProps = { children: ReactNode; disabled?: boolean; + onClick?: (event: MouseEvent) => void; }; -export const Button = ({ children, disabled = false }: ButtonProps) => ( - +export const Button = ({ children, onClick, disabled = false }: ButtonProps) => ( + ); diff --git a/src/shared/modal-form/ModalForm.module.scss b/src/shared/modal-form/ModalForm.module.scss index 7251b0068..72a1f0258 100644 --- a/src/shared/modal-form/ModalForm.module.scss +++ b/src/shared/modal-form/ModalForm.module.scss @@ -1,5 +1,6 @@ .modal { display: flex; + position: fixed; align-items: center; z-index: 1; left: 0; diff --git a/src/shared/modal-form/ModalForm.tsx b/src/shared/modal-form/ModalForm.tsx index ee90553df..88699e282 100644 --- a/src/shared/modal-form/ModalForm.tsx +++ b/src/shared/modal-form/ModalForm.tsx @@ -1,17 +1,20 @@ -import React, { ReactNode } from 'react'; +import React, { ReactNode, MouseEvent } from 'react'; import cn from 'clsx'; import s from './ModalForm.module.scss'; type ModalFormProps = { visible: boolean; children: ReactNode; + onClose?: (event: MouseEvent) => void; }; -export const ModalForm = ({ visible = true, children }: ModalFormProps) => { +export const ModalForm = ({ visible = true, onClose, children }: ModalFormProps) => { return (
- × + + × + {children}
diff --git a/src/shared/statefull-modal-form/StatefullModalForm.module.scss b/src/shared/statefull-modal-form/StatefullModalForm.module.scss new file mode 100644 index 000000000..0247a58cc --- /dev/null +++ b/src/shared/statefull-modal-form/StatefullModalForm.module.scss @@ -0,0 +1,10 @@ +.statefull-modal { + display: flex; + flex-direction: column; + gap: 10px; + align-items: center; + + input { + align-self: stretch; + } +} diff --git a/src/shared/statefull-modal-form/StatefullModalForm.tsx b/src/shared/statefull-modal-form/StatefullModalForm.tsx new file mode 100644 index 000000000..7136f5b7f --- /dev/null +++ b/src/shared/statefull-modal-form/StatefullModalForm.tsx @@ -0,0 +1,24 @@ +import React, { useState, FormEvent } from 'react'; +import { Button } from '../button/Button'; +import { TextInput } from '../text-input/TextInput'; +import { ModalForm } from '../modal-form/ModalForm'; +import s from './StatefullModalForm.module.scss'; + +export const StatefullModalForm = () => { + const [inputValue, setInputValue] = useState(''); + const [isModalFormVisible, setIsModalFormVisible] = useState(false); + + const openModalForm = () => setIsModalFormVisible(true); + const closeModalForm = () => setIsModalFormVisible(false); + const saveInputValue = (event: FormEvent) => setInputValue(event.currentTarget.value); + + return ( +
+ + + +

{inputValue}

+
+
+ ); +}; diff --git a/src/shared/text-input/TextInput.module.scss b/src/shared/text-input/TextInput.module.scss new file mode 100644 index 000000000..34d0b6c2d --- /dev/null +++ b/src/shared/text-input/TextInput.module.scss @@ -0,0 +1,12 @@ +.text-input { + padding: 8px; + font-size: 1em; + border: 2px solid #ccc; + border-radius: 5px; + -webkit-transition: 0.5s; + transition: 0.5s; + outline: none; + &:focus { + border: 2px solid #555; + } +} diff --git a/src/shared/text-input/TextInput.tsx b/src/shared/text-input/TextInput.tsx new file mode 100644 index 000000000..ce435778c --- /dev/null +++ b/src/shared/text-input/TextInput.tsx @@ -0,0 +1,11 @@ +import React, { FormEvent } from 'react'; +import s from './TextInput.module.scss'; + +type TextInputProps = { + value: string; + onInput?: (event: FormEvent) => void; +}; + +export const TextInput = ({ value, onInput }: TextInputProps) => ( + +); diff --git a/src/stories/InputToModal.stories.ts b/src/stories/InputToModal.stories.ts new file mode 100644 index 000000000..316fe5eb7 --- /dev/null +++ b/src/stories/InputToModal.stories.ts @@ -0,0 +1,20 @@ +import type { Meta } from '@storybook/react'; + +import { StatefullModalForm } from '../shared/statefull-modal-form/StatefullModalForm'; + +const meta: Meta = { + component: StatefullModalForm, + title: 'Общее задание/Состояние модального окна', + tags: ['autodocs'], + parameters: { + docs: { + story: { + height: '500px', + }, + }, + }, +}; + +export default meta; + +export const Test = {}; From fbeb9b59a8b971662289bd792fc6932a1ea0a2ca Mon Sep 17 00:00:00 2001 From: Igor Aralov Date: Mon, 2 Dec 2024 23:49:12 +0300 Subject: [PATCH 13/27] Add Theme Switcher --- src/app/App.css | 42 ++++++++++----------- src/app/App.tsx | 18 +++++---- src/app/index.css | 28 ++++++++++---- src/shared/header/Header.module.scss | 4 +- src/shared/header/Header.tsx | 10 ++++- src/shared/layout/Layout.module.scss | 3 +- src/shared/logo/Logo.module.scss | 4 +- src/shared/providers/ThemeProvider.tsx | 27 +++++++++++++ src/shared/theme-switcher/ThemeSwitcher.tsx | 9 +++++ src/stories/Header.stories.ts | 17 --------- src/stories/Header.stories.tsx | 26 +++++++++++++ src/stories/Layout.stories.ts | 13 ------- src/stories/Layout.stories.tsx | 22 +++++++++++ src/stories/theme.css | 17 +++++++++ 14 files changed, 168 insertions(+), 72 deletions(-) create mode 100644 src/shared/providers/ThemeProvider.tsx create mode 100644 src/shared/theme-switcher/ThemeSwitcher.tsx delete mode 100644 src/stories/Header.stories.ts create mode 100644 src/stories/Header.stories.tsx delete mode 100644 src/stories/Layout.stories.ts create mode 100644 src/stories/Layout.stories.tsx create mode 100644 src/stories/theme.css diff --git a/src/app/App.css b/src/app/App.css index 78b8850cf..e6eb966e5 100644 --- a/src/app/App.css +++ b/src/app/App.css @@ -1,38 +1,38 @@ .App { - text-align: center; + text-align: center; } .App-logo { - height: 40vmin; - pointer-events: none; + height: 40vmin; + pointer-events: none; } @media (prefers-reduced-motion: no-preference) { - .App-logo { - animation: App-logo-spin infinite 20s linear; - } + .App-logo { + animation: App-logo-spin infinite 20s linear; + } } .App-header { - background-color: #282c34; - min-height: 100vh; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - font-size: calc(10px + 2vmin); - color: white; + background-color: var(--background-color); + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + font-size: calc(10px + 2vmin); + color: var(--color); } .App-link { - color: #61dafb; + color: #61dafb; } @keyframes App-logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } } diff --git a/src/app/App.tsx b/src/app/App.tsx index 94a44b7bf..58606b706 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -2,12 +2,16 @@ import React from 'react'; import logo from './logo.svg'; import './App.css'; +import { ThemeProvider } from '../shared/providers/ThemeProvider'; +import { ThemeSwitcher } from '../shared/theme-switcher/ThemeSwitcher'; + function App() { return ( -
-
- logo -

+ +

+
+ + logo

Игорь Аралов (igor.aralov@rambler.ru)

Научиться разрабатывать веб-приложения при помощи React

@@ -15,9 +19,9 @@ function App() { на Delphi

Интересуюсь фронтенд и фуллстек разработкой на js/ts

-

-
-
+
+
+ ); } diff --git a/src/app/index.css b/src/app/index.css index 4b326a5a4..1c6785007 100644 --- a/src/app/index.css +++ b/src/app/index.css @@ -1,13 +1,25 @@ +[data-theme='light'] { + --background-color: #fff; + --color: #000; + --logo-bg-color: #f4f4f4; + --logo-text-color: #333; +} + +[data-theme='dark'] { + --background-color: #333; + --color: #fff; + --logo-bg-color: #333; + --logo-text-color: #f4f4f4; +} + body { - margin: 0; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', - 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', - 'Helvetica Neue', sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', + 'Droid Sans', 'Helvetica Neue', sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } code { - font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', - monospace; + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; } diff --git a/src/shared/header/Header.module.scss b/src/shared/header/Header.module.scss index 9d699ac7c..f8c86e29d 100644 --- a/src/shared/header/Header.module.scss +++ b/src/shared/header/Header.module.scss @@ -1,8 +1,10 @@ .header { + display:flex; + justify-content: space-between; position: -webkit-sticky; position: sticky; top: 0; - background-color: #333; + background-color: var(--header-bg-color); color: white; padding: 10px; min-height: 50px; diff --git a/src/shared/header/Header.tsx b/src/shared/header/Header.tsx index 456d1021a..086319e1f 100644 --- a/src/shared/header/Header.tsx +++ b/src/shared/header/Header.tsx @@ -1,9 +1,15 @@ import React from 'react'; import { Logo } from '../logo/Logo'; +import { ThemeSwitcher } from '../theme-switcher/ThemeSwitcher'; import s from './Header.module.scss'; -type HeaderProps = { +export type HeaderProps = { showLogo: boolean; }; -export const Header = ({ showLogo = true }: HeaderProps) =>
{showLogo && }
; +export const Header = ({ showLogo = true }: HeaderProps) => ( +
+ {showLogo && } + +
+); diff --git a/src/shared/layout/Layout.module.scss b/src/shared/layout/Layout.module.scss index 407c9e5bf..9d2e007cd 100644 --- a/src/shared/layout/Layout.module.scss +++ b/src/shared/layout/Layout.module.scss @@ -1,5 +1,6 @@ .content { margin: 0; padding: 20px; - background-color: #f4f4f4; + background-color: var(--layout-bg-color); + color: var(--color); } diff --git a/src/shared/logo/Logo.module.scss b/src/shared/logo/Logo.module.scss index 6d0626cba..2b11759ff 100644 --- a/src/shared/logo/Logo.module.scss +++ b/src/shared/logo/Logo.module.scss @@ -1,12 +1,12 @@ .logo { width: 80px; height: 50px; - background-color: #f4f4f4; + background-color: var(--logo-bg-color); border-radius: 50%; display: flex; justify-content: center; align-items: center; - color: #333; + color: var(--logo-text-color); font-family: 'Courier New', Courier, monospace; font-size: 1.35rem; font-weight: bold; diff --git a/src/shared/providers/ThemeProvider.tsx b/src/shared/providers/ThemeProvider.tsx new file mode 100644 index 000000000..1bdb4290e --- /dev/null +++ b/src/shared/providers/ThemeProvider.tsx @@ -0,0 +1,27 @@ +import React, { createContext, useState, useEffect, useContext, ReactNode } from 'react'; + +type Theme = 'dark' | 'light'; + +type ThemeContextType = { theme: Theme; toggleTheme: () => void }; + +type ThemeProviderProps = { + children: ReactNode; +}; + +const ThemeContext = createContext(null); + +export const useTheme = () => useContext(ThemeContext); + +export const ThemeProvider = ({ children }: ThemeProviderProps) => { + const [theme, setTheme] = useState('light'); + + useEffect(() => { + document.documentElement.setAttribute('data-theme', theme); + }, [theme]); + + const toggleTheme = () => { + setTheme((prevTheme) => (prevTheme === 'dark' ? 'light' : 'dark')); + }; + + return {children}; +}; diff --git a/src/shared/theme-switcher/ThemeSwitcher.tsx b/src/shared/theme-switcher/ThemeSwitcher.tsx new file mode 100644 index 000000000..87d287e59 --- /dev/null +++ b/src/shared/theme-switcher/ThemeSwitcher.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import { useTheme } from '../providers/ThemeProvider'; +import { Button } from '../button/Button'; + +export const ThemeSwitcher = () => { + const { theme, toggleTheme } = useTheme(); + + return ; +}; diff --git a/src/stories/Header.stories.ts b/src/stories/Header.stories.ts deleted file mode 100644 index 542e67e0b..000000000 --- a/src/stories/Header.stories.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { Meta } from '@storybook/react'; - -import { Header } from '../shared/header/Header'; - -const meta: Meta = { - component: Header, - title: 'Общее задание/Header', - tags: ['autodocs'], -}; - -export default meta; - -export const WithLogo = { - args: { - showLogo: true, - }, -}; diff --git a/src/stories/Header.stories.tsx b/src/stories/Header.stories.tsx new file mode 100644 index 000000000..2c288d4d3 --- /dev/null +++ b/src/stories/Header.stories.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import type { Meta } from '@storybook/react'; + +import { Header, HeaderProps } from '../shared/header/Header'; +import { ThemeProvider } from '../shared/providers/ThemeProvider'; +import './theme.css'; + +const ThemedHeader = (data: HeaderProps) => ( + +
+ +); + +const meta: Meta = { + component: ThemedHeader, + title: 'Общее задание/Header', + tags: ['autodocs'], +}; + +export default meta; + +export const WithLogo = { + args: { + showLogo: true, + }, +}; diff --git a/src/stories/Layout.stories.ts b/src/stories/Layout.stories.ts deleted file mode 100644 index 3cf76fca1..000000000 --- a/src/stories/Layout.stories.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { Meta } from '@storybook/react'; - -import { Layout } from '../shared/layout/Layout'; - -const meta: Meta = { - component: Layout, - title: 'Общее задание/Layout', - tags: ['autodocs'], -}; - -export default meta; - -export const LayoutWithContent = {}; diff --git a/src/stories/Layout.stories.tsx b/src/stories/Layout.stories.tsx new file mode 100644 index 000000000..22f7c3b6d --- /dev/null +++ b/src/stories/Layout.stories.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import type { Meta } from '@storybook/react'; + +import { Layout } from '../shared/layout/Layout'; +import { ThemeProvider } from '../shared/providers/ThemeProvider'; +import './theme.css'; + +const ThemedLayout = () => ( + + + +); + +const meta: Meta = { + component: ThemedLayout, + title: 'Общее задание/Layout', + tags: ['autodocs'], +}; + +export default meta; + +export const LayoutWithContent = {}; diff --git a/src/stories/theme.css b/src/stories/theme.css new file mode 100644 index 000000000..a5531e7c6 --- /dev/null +++ b/src/stories/theme.css @@ -0,0 +1,17 @@ +[data-theme='light'] { + --background-color: #fff; + --color: #000; + --logo-bg-color: #333 ; + --logo-text-color: #f4f4f4; + --header-bg-color: #dbdbdb; + --layout-bg-color: #fafafa; +} + +[data-theme='dark'] { + --background-color: #333; + --color: #fff; + --logo-bg-color: #f4f4f4; + --logo-text-color: #333; + --header-bg-color: #333; + --layout-bg-color: #4e4e4e; +} From 932d223a3ed0a7943b5a4d13521f713471977801 Mon Sep 17 00:00:00 2001 From: Igor Aralov Date: Tue, 3 Dec 2024 22:43:39 +0300 Subject: [PATCH 14/27] Add Lang Switcher --- src/app/App.css | 10 +- src/app/App.tsx | 57 +++- src/app/index.css | 4 - src/shared/header/Header.module.scss | 13 +- src/shared/header/Header.tsx | 8 +- src/shared/lang-switcher/LangSwitcher.tsx | 9 + src/shared/layout/Layout.tsx | 331 ++-------------------- src/shared/providers/LangProvider.tsx | 38 +++ src/stories/Header.stories.tsx | 11 +- src/stories/Layout.stories.tsx | 11 +- 10 files changed, 157 insertions(+), 335 deletions(-) create mode 100644 src/shared/lang-switcher/LangSwitcher.tsx create mode 100644 src/shared/providers/LangProvider.tsx diff --git a/src/app/App.css b/src/app/App.css index e6eb966e5..05a3f9709 100644 --- a/src/app/App.css +++ b/src/app/App.css @@ -1,5 +1,7 @@ .App { text-align: center; + background-color: var(--background-color); + color: var(--color); } .App-logo { @@ -14,14 +16,18 @@ } .App-header { - background-color: var(--background-color); min-height: 100vh; display: flex; flex-direction: column; align-items: center; justify-content: center; font-size: calc(10px + 2vmin); - color: var(--color); +} + +.App-nav { + display: flex; + justify-content: center; + gap: 15px; } .App-link { diff --git a/src/app/App.tsx b/src/app/App.tsx index 58606b706..2929056c4 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -3,24 +3,55 @@ import logo from './logo.svg'; import './App.css'; import { ThemeProvider } from '../shared/providers/ThemeProvider'; +import { LangProvider, Content, useLang } from '../shared/providers/LangProvider'; import { ThemeSwitcher } from '../shared/theme-switcher/ThemeSwitcher'; +import { LangSwitcher } from '../shared/lang-switcher/LangSwitcher'; + +const MainContent = () => { + const content: Content = { + fullname: { ru: 'Игорь Аралов', en: 'Igor Aralov' }, + target: { + ru: 'Научиться разрабатывать веб-приложения при помощи React', + en: 'Learn to develop web applications using React', + }, + skills: { + ru: 'Профессионально работаю SQL разработчиком уже 10 лет и принимаю участие в разработке десктопного приложения на Delphi', + en: 'I have been working professionally as an SQL developer for 10 years and am involved in the development of a desktop application using Delphi.', + }, + hobby: { + ru: 'Интересуюсь фронтенд и фуллстек разработкой на js/ts', + en: "I'm interested in frontend and full-stack development using js/ts", + }, + }; + + const { useContent } = useLang(); + const l = useContent(content); + + return ( + <> +

{l('fullname')} (igor.aralov@rambler.ru)

+

{l('target')}

+

{l('skills')}

+

{l('hobby')}

+ + ); +}; function App() { return ( -
-
- - logo -

Игорь Аралов (igor.aralov@rambler.ru)

-

Научиться разрабатывать веб-приложения при помощи React

-

- Профессионально работаю SQL разработчиком уже 10 лет и принимаю участие в разработке десктопного приложения - на Delphi -

-

Интересуюсь фронтенд и фуллстек разработкой на js/ts

-
-
+ +
+
+
+ + +
+ logo + +
+
+
); } diff --git a/src/app/index.css b/src/app/index.css index 1c6785007..0ca6b3986 100644 --- a/src/app/index.css +++ b/src/app/index.css @@ -1,15 +1,11 @@ [data-theme='light'] { --background-color: #fff; --color: #000; - --logo-bg-color: #f4f4f4; - --logo-text-color: #333; } [data-theme='dark'] { --background-color: #333; --color: #fff; - --logo-bg-color: #333; - --logo-text-color: #f4f4f4; } body { diff --git a/src/shared/header/Header.module.scss b/src/shared/header/Header.module.scss index f8c86e29d..84cbf3220 100644 --- a/src/shared/header/Header.module.scss +++ b/src/shared/header/Header.module.scss @@ -1,5 +1,5 @@ .header { - display:flex; + display: flex; justify-content: space-between; position: -webkit-sticky; position: sticky; @@ -10,3 +10,14 @@ min-height: 50px; z-index: 1000; } + +.left { + display: flex; + gap: 10px; +} + +.right { + display: flex; + justify-content: end; + gap: 10px; +} diff --git a/src/shared/header/Header.tsx b/src/shared/header/Header.tsx index 086319e1f..658f18145 100644 --- a/src/shared/header/Header.tsx +++ b/src/shared/header/Header.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { Logo } from '../logo/Logo'; import { ThemeSwitcher } from '../theme-switcher/ThemeSwitcher'; +import { LangSwitcher } from '../lang-switcher/LangSwitcher'; import s from './Header.module.scss'; export type HeaderProps = { @@ -9,7 +10,10 @@ export type HeaderProps = { export const Header = ({ showLogo = true }: HeaderProps) => (
- {showLogo && } - +
{showLogo && }
+
+ + +
); diff --git a/src/shared/lang-switcher/LangSwitcher.tsx b/src/shared/lang-switcher/LangSwitcher.tsx new file mode 100644 index 000000000..65538061e --- /dev/null +++ b/src/shared/lang-switcher/LangSwitcher.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import { useLang } from '../providers/LangProvider'; +import { Button } from '../button/Button'; + +export const LangSwitcher = () => { + const { lang, toggleLang } = useLang(); + + return ; +}; diff --git a/src/shared/layout/Layout.tsx b/src/shared/layout/Layout.tsx index 9af8a9b13..08df235a1 100644 --- a/src/shared/layout/Layout.tsx +++ b/src/shared/layout/Layout.tsx @@ -1,315 +1,36 @@ import React from 'react'; import { Header } from '../header/Header'; +import { Content, useLang } from '../../shared/providers/LangProvider'; import s from './Layout.module.scss'; -const Content = () => ( -

-

Lorem ipsum dolor sit amet.

-

Odit necessitatibus iure quas quaerat!

-

Quae vitae deserunt vero accusantium?

-

Id laborum quaerat rem laboriosam.

-

Molestias saepe assumenda optio ab!

-

Facilis nostrum natus consectetur quam.

-

Doloremque illum ducimus ut assumenda.

-

Numquam doloribus autem nemo sint.

-

Repellendus ex odio aperiam pariatur.

-

Assumenda natus ratione labore tempora.

-

Delectus, cumque. Enim, aut iste.

-

Ipsa fugit ad temporibus reprehenderit.

-

Quos beatae assumenda fuga repellat.

-

Odit odio velit ducimus repellat?

-

Placeat possimus vero alias perferendis.

-

Facilis, inventore. Ratione, delectus illum?

-

Necessitatibus nisi harum assumenda eligendi!

-

Nisi laboriosam et voluptates. Repudiandae.

-

Commodi illo dolore ipsa eos.

-

Optio incidunt veniam officiis reiciendis.

-

Illo odio sapiente eum aliquam.

-

Et aspernatur enim provident sit.

-

Totam maiores dicta magnam deserunt.

-

Doloremque amet magni tempore expedita.

-

Dolores asperiores expedita aut magnam!

-

Consectetur tempora atque perferendis odio.

-

Cum eius officiis repudiandae fuga.

-

Officiis repellat ducimus architecto culpa.

-

Ducimus odio vero corrupti error!

-

Dolore perspiciatis consequuntur culpa commodi!

-

Explicabo sapiente laudantium quae atque.

-

Hic reiciendis dignissimos vitae veritatis!

-

Provident veniam ut ad consectetur.

-

Maxime nobis voluptates deleniti consectetur!

-

Neque soluta itaque voluptatum alias!

-

Ad, ipsa quo. Nulla, laborum.

-

Inventore voluptatem quas eveniet rerum!

-

Omnis modi possimus animi voluptatum!

-

Voluptatem natus alias fugiat accusamus.

-

Itaque eos maiores corporis unde!

-

Cum repudiandae ad autem et!

-

Cum provident placeat veniam suscipit.

-

Animi, possimus. Distinctio, quos necessitatibus?

-

Consequatur dolore impedit dicta officiis!

-

Maxime eligendi voluptatum aspernatur repudiandae!

-

Maiores accusantium officia veniam odit!

-

Dicta, eius. Dolore, maxime optio?

-

Accusamus ad nihil numquam quae.

-

Atque, quidem! Perspiciatis, non unde.

-

Laboriosam eum consequuntur saepe iusto.

-

Facere aliquam sit ad omnis.

-

Consectetur voluptate doloribus nemo et!

-

Accusamus debitis quaerat beatae quas.

-

Ipsa consectetur tenetur dolorem omnis.

-

Blanditiis ad asperiores culpa at.

-

Cupiditate itaque iure doloribus hic.

-

Numquam hic incidunt voluptate fugiat?

-

Illo distinctio explicabo porro maiores.

-

Numquam ea totam officiis voluptatibus.

-

Ex recusandae est perspiciatis quos!

-

Dolores iste recusandae modi facilis?

-

Asperiores id vitae odio officiis.

-

Blanditiis numquam enim accusantium quia.

-

Delectus quas tenetur iure voluptatibus?

-

Illo aliquam inventore exercitationem quia.

-

Reprehenderit, voluptates modi! Molestias, quae!

-

Iure dicta repellat facilis in!

-

Eos fugit tempora dolor molestias.

-

Earum neque consectetur magnam exercitationem!

-

Facere repudiandae vel assumenda odit!

-

Dolore expedita officiis sed voluptatem.

-

Ratione minima odit blanditiis voluptatum!

-

Quibusdam laborum reiciendis placeat iste!

-

Architecto obcaecati eaque nihil non!

-

Dolore nihil placeat alias qui!

-

Beatae veritatis assumenda illo ipsum?

-

Blanditiis animi facilis adipisci suscipit.

-

Voluptatibus, architecto aut. Aliquam, molestias!

-

Reprehenderit illo reiciendis minima. Tempore.

-

Commodi dicta totam tenetur dolore?

-

Repellat, ratione corporis! Placeat, soluta.

-

Totam similique pariatur magnam modi.

-

Expedita exercitationem ipsum debitis velit.

-

Magni perferendis aut deserunt alias.

-

Consectetur sint aspernatur dolor quidem.

-

Incidunt porro voluptates assumenda odio!

-

Quae nemo laborum nihil nulla.

-

Error voluptas itaque dicta suscipit.

-

Rem magnam amet dignissimos consequuntur.

-

Tempore corrupti nesciunt reiciendis ratione!

-

Eum quasi fugit tempore adipisci.

-

Vel fugiat unde ipsa repellat.

-

Suscipit, vero? Voluptatum, suscipit id!

-

Eligendi itaque magnam et? Voluptatibus?

-

Aperiam laborum tempore explicabo reiciendis.

-

Cupiditate esse excepturi ab eos.

-

Vero animi soluta omnis praesentium!

-

Incidunt at laborum veritatis quis.

-

Facilis dolorum sunt dolores doloribus.

-

Ab officiis eaque iste doloribus.

-

Assumenda necessitatibus tempore fugiat rerum.

-

Nam quasi nihil minus ipsum.

-

Voluptatem temporibus possimus assumenda cum?

-

Tenetur et eos possimus fugit?

-

Consequatur dolorum at odit a.

-

Consequatur exercitationem ipsum nisi expedita.

-

Modi, neque? Nesciunt, nobis laborum.

-

Optio est tempore amet ratione?

-

Dolor repudiandae laboriosam ullam nobis!

-

Iste assumenda nostrum est suscipit?

-

Porro veniam quidem iste in.

-

Praesentium quo molestias dolorum odio.

-

Minus veniam culpa repellendus esse.

-

Provident tempore commodi nemo corporis?

-

Laudantium quibusdam sunt aliquam ab?

-

Architecto consequuntur voluptas distinctio delectus?

-

Dignissimos excepturi deleniti reprehenderit tempora.

-

Iure atque deserunt consequatur laborum?

-

Ullam, modi vero. Error, doloremque!

-

Veritatis fugiat odit libero exercitationem!

-

Sapiente placeat perferendis laudantium error!

-

Accusamus vero magnam illo molestiae?

-

Doloremque minus exercitationem error neque?

-

Quam corrupti culpa repellat adipisci?

-

Id harum fugit tempora quas?

-

Quo eveniet aperiam dignissimos cupiditate.

-

Aut quasi quidem in laborum?

-

Laudantium necessitatibus totam itaque impedit.

-

Deserunt quo porro quasi ut.

-

Repellendus doloremque nam enim laudantium.

-

Modi quae nesciunt ipsam animi!

-

Minima aut repellat ab reiciendis?

-

Doloremque eaque sequi doloribus laboriosam.

-

Ratione repudiandae natus aliquid sunt?

-

Velit ratione vel dolore maxime.

-

Temporibus, dolore. Aspernatur, voluptate nisi?

-

Eius quos sed delectus sit.

-

Ut cupiditate sequi esse ipsum.

-

Veniam voluptas exercitationem quibusdam animi.

-

Cum temporibus nobis fugiat repellat?

-

Vitae, atque sapiente! Omnis, fugit.

-

Perspiciatis consequatur optio aut sunt.

-

Nesciunt itaque eaque tenetur aliquam!

-

Fugit accusamus veniam culpa adipisci!

-

Blanditiis numquam ipsam culpa optio.

-

Molestiae eum fugiat architecto rerum!

-

Saepe fugit dolor totam corporis!

-

Quaerat aliquam vero officiis a.

-

Ab magni modi quas sit.

-

Reiciendis suscipit unde ducimus quasi.

-

Molestias laudantium facilis dicta consectetur.

-

Distinctio adipisci autem accusamus quibusdam!

-

Possimus hic eaque illum a?

-

Culpa labore reprehenderit voluptate tempore?

-

Quod, doloremque. Natus, ipsa quo.

-

Illum molestias ducimus sequi magnam.

-

Explicabo nostrum amet quia hic.

-

Vero aperiam tempora quos reiciendis.

-

Placeat fuga ea quos qui.

-

Laboriosam, dolores? Nesciunt, facere neque?

-

Praesentium voluptas animi dolor repudiandae!

-

Velit ullam suscipit consequuntur adipisci.

-

Eius commodi quisquam doloribus cupiditate!

-

Illo debitis perferendis quis iusto!

-

Aut laudantium mollitia nobis assumenda.

-

Nisi, quasi! Temporibus, ducimus quam.

-

In pariatur tenetur ducimus rem?

-

Unde dolore eos accusamus molestiae.

-

Error voluptatum ipsam sunt ex.

-

Voluptates temporibus vitae magni earum!

-

Alias ipsam tenetur placeat quae.

-

Explicabo consectetur vero quidem saepe.

-

Facilis consectetur nulla culpa veritatis?

-

Laudantium dolorem animi vel tempore!

-

Iste eius ex mollitia quis?

-

Eum error eligendi aspernatur optio!

-

Delectus suscipit maxime minus quo.

-

Ad cupiditate ducimus aspernatur aperiam.

-

Obcaecati accusamus eius natus cupiditate!

-

Voluptatem maxime voluptatum odio blanditiis?

-

Illum sapiente amet officia ipsa.

-

Sit exercitationem minima quaerat nobis!

-

Nulla nesciunt quia ea temporibus!

-

Omnis totam magnam laboriosam? Doloribus!

-

Voluptatem excepturi dolores culpa distinctio.

-

Accusamus alias architecto obcaecati repudiandae.

-

Dolores mollitia corporis laborum hic?

-

Laboriosam, beatae assumenda? Officia, vel!

-

Provident excepturi minus fugiat iusto?

-

Similique voluptas doloremque consequuntur optio?

-

Ea voluptates dolor numquam quasi.

-

Cupiditate distinctio nesciunt temporibus molestiae?

-

Laborum harum autem expedita excepturi?

-

Tenetur consequatur distinctio alias itaque.

-

Nam officiis minima modi totam.

-

Velit deleniti accusantium illum natus?

-

Exercitationem, alias? Voluptate, quis nulla!

-

Asperiores odit quasi itaque sed!

-

Incidunt nulla quae odit sequi.

-

Incidunt fugit omnis neque fugiat?

-

Ipsum esse veniam atque nam?

-

Minus sequi consectetur inventore repellat!

-

Alias corporis harum ad optio.

-

Aliquam sed mollitia magnam pariatur?

-

Ipsum sed odio sunt officiis.

-

Pariatur numquam facilis dolorum excepturi.

-

Nemo quae culpa incidunt laboriosam.

-

Fuga provident molestias a esse?

-

Pariatur, sequi quod. Debitis, quos.

-

Earum voluptates dolorum dolor rem!

-

Eveniet dolor quis aperiam accusamus?

-

Doloremque expedita id vero quisquam!

-

Consequatur facere aperiam iusto odit?

-

Nemo harum saepe ex totam!

-

Perferendis nisi sunt id voluptatum!

-

Quam omnis esse molestias doloremque.

-

Eligendi adipisci odio hic quia?

-

Quaerat libero accusamus debitis consequuntur.

-

Voluptatem commodi veritatis quos necessitatibus.

-

Excepturi sequi similique vel nihil!

-

Voluptatem vel nostrum aliquam nihil.

-

Quos voluptatum quidem sequi nulla?

-

Ipsum soluta autem sapiente odio.

-

Non aut accusantium autem impedit.

-

Hic repellendus eligendi harum deleniti.

-

Vero provident voluptatibus quae. Cumque.

-

Voluptatibus doloremque beatae in necessitatibus.

-

Aut ducimus tempora magnam consequatur.

-

Repudiandae omnis corporis obcaecati nobis?

-

Quis similique cupiditate corrupti dignissimos.

-

Aliquid aperiam non laborum repudiandae.

-

Delectus ratione error officiis blanditiis!

-

Reiciendis labore dolores dignissimos voluptatem.

-

Optio, dolores ad. Explicabo, impedit?

-

Quas explicabo autem maiores perferendis!

-

Expedita sint optio minus debitis!

-

Veritatis voluptates vitae autem deleniti?

-

Nemo, laborum iste. Nam, et?

-

Culpa excepturi porro laborum at!

-

A suscipit numquam eius ab?

-

Sapiente distinctio est sed aliquam!

-

Ea amet consectetur eveniet doloremque?

-

Alias consequuntur obcaecati ea praesentium.

-

Tenetur nostrum fugiat eum ullam?

-

Eum enim itaque animi at?

-

Incidunt reiciendis ratione inventore neque.

-

Veritatis repellendus mollitia nisi reiciendis!

-

Ipsum numquam earum ea ipsam.

-

Consequuntur officia at deleniti eos!

-

Amet facere aliquid inventore exercitationem.

-

Neque reprehenderit dolores voluptatum aperiam.

-

Officia velit vel modi earum.

-

Adipisci, quibusdam. Magnam, asperiores voluptate?

-

Nulla placeat cupiditate odit delectus.

-

Illo velit iste sapiente mollitia!

-

Totam incidunt facilis ipsum accusantium.

-

Blanditiis, ducimus! A, quidem repudiandae?

-

Enim incidunt soluta alias vel!

-

Esse accusamus eveniet nihil quis.

-

Eaque dolore repellendus nulla sit.

-

Eveniet corporis et iusto neque.

-

Dignissimos, sequi. In, soluta explicabo.

-

Dolorum tenetur error enim aperiam?

-

Enim porro harum itaque laboriosam!

-

Deleniti necessitatibus temporibus mollitia dolorum.

-

Natus, eos! Nobis, consequuntur enim!

-

Laborum ullam architecto fuga quia.

-

Inventore, molestiae. Repellendus, quaerat modi?

-

Quis hic molestiae cupiditate suscipit.

-

Dolorum modi soluta et amet.

-

Quo labore quis modi dolores?

-

Quos optio expedita harum nihil!

-

Nesciunt laborum quia id dolore?

-

Tempore ipsam eaque facere expedita!

-

Sequi assumenda quod tempore natus?

-

Cumque maiores reprehenderit inventore optio.

-

Itaque repudiandae repellat expedita ipsum!

-

Repudiandae dicta nihil optio magnam.

-

Aliquid beatae dolores harum nobis?

-

Molestias libero id asperiores fuga.

-

Atque sed at sunt voluptate!

-

Quisquam nisi sint voluptate quis.

-

Sunt asperiores quis fugit corporis.

-

Inventore odio consequatur ratione quibusdam.

-

Vel nobis tenetur facere architecto?

-

Maxime tempore laudantium ducimus odit.

-

Sit corporis veritatis omnis itaque.

-

Quod, nemo sequi. At, distinctio!

-

Aut cupiditate perspiciatis qui natus.

-

Incidunt veniam nam quae expedita!

-

Amet fugit veniam eveniet fugiat.

-

Ipsam obcaecati ea iure hic?

-

Asperiores dicta pariatur eum vitae?

-

Corrupti tempore obcaecati beatae explicabo.

-

Et eius cumque quidem minus!

-

Ipsam id adipisci voluptas soluta?

-

Asperiores earum voluptas ducimus dignissimos!

-

Aspernatur esse autem mollitia ut?

-

Expedita ex atque soluta at?

-

Laboriosam laborum quasi ipsum est.

-

-); +const content: Content = { + text: { + en: `As the sun began to set over the tranquil village of Willowbrook, a sense of anticipation filled the air. The annual harvest festival was about to commence, and the villagers were bustling with excitement. Children ran through the fields, their laughter echoing like a melody of pure joy. Stalls were being set up along the main square, offering an array of handmade crafts, freshly baked goods, and the season’s finest produce. +At the heart of the village stood an ancient oak tree, its branches sprawling wide like a guardian of time. Beneath this tree, the elders gathered, sharing stories of yore and wisdom passed down through generations. Amelia, a young girl with sparkling eyes and boundless curiosity, sat among them, soaking in every word. She loved listening to tales of bravery, love, and adventure, dreaming that one day, she too would have stories to tell. +As the evening unfolded, the sky painted itself in hues of orange, pink, and purple, casting a magical glow over Willowbrook. The village musicians took their place, their instruments gleaming in the fading light. The first notes of a lively tune filled the air, and soon, everyone was dancing. Amelia twirled and spun, her laughter blending with the music, her heart filled with the simple happiness that only such moments can bring. +As the night grew darker, lanterns were lit, casting a warm, golden light over the festivities. The aroma of roasted chestnuts and spiced cider wafted through the air, inviting everyone to partake in the feast. Friends and families gathered around long wooden tables, sharing food and stories, creating memories that would last a lifetime. +Amidst the joy and celebration, Amelia’s thoughts wandered to the future. She knew that as long as the spirit of the harvest festival lived on, the bond within the community would remain strong, and Willowbrook would continue to be a place of love, laughter, and unity. With a heart full of hope and dreams, she smiled, knowing that she was part of something truly special.`, + ru: `Когда солнце начало садиться над тихой деревушкой Виллоубрук, в воздухе витало чувство ожидания. Ежегодный праздник урожая вот-вот начнется, и жители деревни бурлили от волнения. Дети бегали по полям, их смех звучал как мелодия чистой радости. На главной площади устанавливались ларьки, предлагающие ассортимент самодельных поделок, свежевыпеченных товаров и лучших продуктов сезона. +В центре деревни стоял древний дуб, его ветви раскидывались широко, как хранитель времени. Под этим деревом собирались старейшины, рассказывая истории былых времен и мудрость, переданную через поколения. Амелия, молодая девушка с блестящими глазами и безграничным любопытством, сидела среди них, впитывая каждое слово. Она любила слушать истории о храбрости, любви и приключениях, мечтая о том, что однажды у нее тоже будут свои истории. +По мере того как вечер разворачивался, небо окрашивалось в оттенки оранжевого, розового и пурпурного, придавая Виллоубруку магическое сияние. Деревенские музыканты заняли свои места, их инструменты сияли в ускользающем свете. Первые ноты оживленной мелодии заполнили воздух, и вскоре все танцевали. Амелия кружилась и вертелась, ее смех сливался с музыкой, сердце наполнялось простой радостью, которую могут принести только такие моменты. +Когда ночь стала темнее, зажглись фонари, отбрасывая теплый, золотой свет на праздник. Аромат жареных каштанов и пряного сидра витал в воздухе, приглашая всех принять участие в пиршестве. Друзья и семьи собрались вокруг длинных деревянных столов, делясь едой и историями, создавая воспоминания, которые останутся на всю жизнь. +Среди радости и праздника мысли Амелии унеслись в будущее. Она знала, что пока живет дух праздника урожая, связь внутри сообщества останется сильной, и Виллоубрук продолжит быть местом любви, смеха и единства. С сердцем, полным надежд и мечтаний, она улыбалась, зная, что является частью чего-то по-настоящему особенного.`, + }, +}; + +const TestContent = () => { + const { useContent } = useLang(); + const l = useContent(content); + + return
{l('text')}
; +}; export const Layout = () => ( <>
- + + + + ); diff --git a/src/shared/providers/LangProvider.tsx b/src/shared/providers/LangProvider.tsx new file mode 100644 index 000000000..58a7023fb --- /dev/null +++ b/src/shared/providers/LangProvider.tsx @@ -0,0 +1,38 @@ +import React, { createContext, useState, useContext, ReactNode } from 'react'; + +type Lang = 'ru' | 'en'; +type ContentKey = string; +type ContentValue = string; +type LangValues = Partial>; +export type Content = Record; + +type LangContextType = { + lang: Lang; + useContent: (content: Content) => (key: ContentKey) => ContentValue; + toggleLang: () => void; +}; + +type LangProviderProps = { + children: ReactNode; +}; + +const LangContext = createContext(null); + +export const useLang = () => useContext(LangContext); + +const withContent = + (lang: Lang) => + (content: Content) => + (key: ContentKey): ContentValue => + content[key]?.[lang]; + +export const LangProvider = ({ children }: LangProviderProps) => { + const [lang, setLang] = useState('en'); + const toggleLang = () => { + setLang((prevLang) => (prevLang === 'en' ? 'ru' : 'en')); + }; + + return ( + {children} + ); +}; diff --git a/src/stories/Header.stories.tsx b/src/stories/Header.stories.tsx index 2c288d4d3..8f56a2b0a 100644 --- a/src/stories/Header.stories.tsx +++ b/src/stories/Header.stories.tsx @@ -3,16 +3,19 @@ import type { Meta } from '@storybook/react'; import { Header, HeaderProps } from '../shared/header/Header'; import { ThemeProvider } from '../shared/providers/ThemeProvider'; +import { LangProvider } from '../shared/providers/LangProvider'; import './theme.css'; -const ThemedHeader = (data: HeaderProps) => ( +const HeaderWithThemeAndLangProviders = (data: HeaderProps) => ( -
+ +
+ ); -const meta: Meta = { - component: ThemedHeader, +const meta: Meta = { + component: HeaderWithThemeAndLangProviders, title: 'Общее задание/Header', tags: ['autodocs'], }; diff --git a/src/stories/Layout.stories.tsx b/src/stories/Layout.stories.tsx index 22f7c3b6d..69baadb27 100644 --- a/src/stories/Layout.stories.tsx +++ b/src/stories/Layout.stories.tsx @@ -3,16 +3,19 @@ import type { Meta } from '@storybook/react'; import { Layout } from '../shared/layout/Layout'; import { ThemeProvider } from '../shared/providers/ThemeProvider'; +import { LangProvider } from '../shared/providers/LangProvider'; import './theme.css'; -const ThemedLayout = () => ( +const LayoutWithThemeAndLangProviders = () => ( - + + + ); -const meta: Meta = { - component: ThemedLayout, +const meta: Meta = { + component: LayoutWithThemeAndLangProviders, title: 'Общее задание/Layout', tags: ['autodocs'], }; From d19616dd6c0db56ebe4582376edd5d27b49ff073 Mon Sep 17 00:00:00 2001 From: Igor Aralov Date: Wed, 4 Dec 2024 21:13:40 +0300 Subject: [PATCH 15/27] Fix modal form --- src/shared/modal-form/ModalForm.module.scss | 3 --- src/shared/modal-form/ModalForm.tsx | 8 +++----- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/shared/modal-form/ModalForm.module.scss b/src/shared/modal-form/ModalForm.module.scss index 72a1f0258..41b30f745 100644 --- a/src/shared/modal-form/ModalForm.module.scss +++ b/src/shared/modal-form/ModalForm.module.scss @@ -33,6 +33,3 @@ } } -.modal-hide { - display: none; -} diff --git a/src/shared/modal-form/ModalForm.tsx b/src/shared/modal-form/ModalForm.tsx index 88699e282..2f5171ded 100644 --- a/src/shared/modal-form/ModalForm.tsx +++ b/src/shared/modal-form/ModalForm.tsx @@ -1,5 +1,4 @@ import React, { ReactNode, MouseEvent } from 'react'; -import cn from 'clsx'; import s from './ModalForm.module.scss'; type ModalFormProps = { @@ -8,9 +7,9 @@ type ModalFormProps = { onClose?: (event: MouseEvent) => void; }; -export const ModalForm = ({ visible = true, onClose, children }: ModalFormProps) => { - return ( -
+export const ModalForm = ({ visible = true, onClose, children }: ModalFormProps) => + !!visible && ( +
× @@ -19,4 +18,3 @@ export const ModalForm = ({ visible = true, onClose, children }: ModalFormProps)
); -}; From d236ab1a7b35c614dc5674643fa2dd1cc0349aa1 Mon Sep 17 00:00:00 2001 From: Igor Aralov Date: Wed, 4 Dec 2024 21:13:40 +0300 Subject: [PATCH 16/27] Fix modal form --- src/shared/modal-form/ModalForm.module.scss | 3 --- src/shared/modal-form/ModalForm.tsx | 8 +++----- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/shared/modal-form/ModalForm.module.scss b/src/shared/modal-form/ModalForm.module.scss index 72a1f0258..41b30f745 100644 --- a/src/shared/modal-form/ModalForm.module.scss +++ b/src/shared/modal-form/ModalForm.module.scss @@ -33,6 +33,3 @@ } } -.modal-hide { - display: none; -} diff --git a/src/shared/modal-form/ModalForm.tsx b/src/shared/modal-form/ModalForm.tsx index 88699e282..2f5171ded 100644 --- a/src/shared/modal-form/ModalForm.tsx +++ b/src/shared/modal-form/ModalForm.tsx @@ -1,5 +1,4 @@ import React, { ReactNode, MouseEvent } from 'react'; -import cn from 'clsx'; import s from './ModalForm.module.scss'; type ModalFormProps = { @@ -8,9 +7,9 @@ type ModalFormProps = { onClose?: (event: MouseEvent) => void; }; -export const ModalForm = ({ visible = true, onClose, children }: ModalFormProps) => { - return ( -
+export const ModalForm = ({ visible = true, onClose, children }: ModalFormProps) => + !!visible && ( +
× @@ -19,4 +18,3 @@ export const ModalForm = ({ visible = true, onClose, children }: ModalFormProps)
); -}; From 8341dd3c48679742a27fa24d0dcc0ec51d11c933 Mon Sep 17 00:00:00 2001 From: Igor Aralov Date: Thu, 5 Dec 2024 21:38:16 +0300 Subject: [PATCH 17/27] =?UTF-8?q?[10]=20=D0=A1=D0=BF=D0=B8=D1=81=D0=BA?= =?UTF-8?q?=D0=B8,=20=D0=BA=D0=BB=D1=8E=D1=87=D0=B8,=20=D1=81=D0=BE=D0=B1?= =?UTF-8?q?=D1=8B=D1=82=D0=B8=D1=8F,=20=D0=BF=D0=BE=D1=80=D1=82=D0=B0?= =?UTF-8?q?=D0=BB=D1=8B=20=D0=9C=D0=BE=D0=B4=D0=B0=D0=BB=D1=8C=D0=BD=D0=BE?= =?UTF-8?q?=D0=B5=20=D0=BE=D0=BA=D0=BD=D0=BE=20=D0=9A=D0=BE=D0=BC=D0=BF?= =?UTF-8?q?=D0=BE=D0=BD=D0=B5=D0=BD=D1=82=20=D1=81=D0=BF=D0=B8=D1=81=D0=BA?= =?UTF-8?q?=D0=B0=20=D1=82=D0=BE=D0=B2=D0=B0=D1=80=D0=BE=D0=B2/=D0=BE?= =?UTF-8?q?=D0=BF=D0=B5=D1=80=D0=B0=D1=86=D0=B8=D0=B9=20=D0=94=D0=B8=D0=BD?= =?UTF-8?q?=D0=B0=D0=BC=D0=B8=D1=87=D0=B5=D1=81=D0=BA=D0=BE=D0=B5=20=D0=B4?= =?UTF-8?q?=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=82?= =?UTF-8?q?=D0=BE=D0=B2=D0=B0=D1=80=D0=BE=D0=B2/=D0=BE=D0=BF=D0=B5=D1=80?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D0=B9=20=D0=9D=D0=BE=D0=B2=D1=8B=D0=B5=20?= =?UTF-8?q?=D0=BE=D0=BF=D0=B5=D1=80=D0=B0=D1=86=D0=B8=D0=B8=20=D0=B4=D0=BE?= =?UTF-8?q?=D0=B1=D0=B0=D0=B2=D0=BB=D1=8F=D1=8E=D1=82=D1=81=D1=8F=20=D1=81?= =?UTF-8?q?=20=D0=BF=D0=BE=D0=BC=D0=BE=D1=89=D1=8C=D1=8E=20IntersectionObs?= =?UTF-8?q?erver?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/homeworks/ts1/3_write.ts | 19 ++- src/homeworks/ts1/data.ts | 122 ++++++++++++++++++ .../OperationContainer.module.scss | 1 - .../OperationDynamicList.module.scss | 5 + .../OperationDynamicList.tsx | 26 ++++ .../OperationListAddMore.module.scss | 5 + .../OperationListAddMore.tsx | 20 +++ .../operation-list/OperationList.module.scss | 5 + .../operation-list/OperationList.tsx | 29 +++++ .../operation/Operation.module.scss | 4 + .../operation/Operation.tsx | 3 +- src/shared/modal-form/ModalForm.tsx | 9 +- .../PortalModalForm.module.scss | 10 ++ .../portal-modal-form/PortalModalForm.tsx | 28 ++++ src/stories/InputToModal.stories.ts | 2 +- src/stories/InputToPortalModal.stories.ts | 13 ++ src/stories/ModalForm.stories.tsx | 2 +- src/stories/OpearationDynamicList.stories.tsx | 13 ++ src/stories/OperationDetail.stories.tsx | 2 +- src/stories/OperationList.stories.tsx | 29 +++++ src/stories/OperationListAddMore.stories.tsx | 13 ++ src/stories/OperationSummary.stories.tsx | 2 +- 22 files changed, 344 insertions(+), 18 deletions(-) create mode 100644 src/shared/icome-expenses-accounting/operation-dynamic-list/OperationDynamicList.module.scss create mode 100644 src/shared/icome-expenses-accounting/operation-dynamic-list/OperationDynamicList.tsx create mode 100644 src/shared/icome-expenses-accounting/operation-list-add-more/OperationListAddMore.module.scss create mode 100644 src/shared/icome-expenses-accounting/operation-list-add-more/OperationListAddMore.tsx create mode 100644 src/shared/icome-expenses-accounting/operation-list/OperationList.module.scss create mode 100644 src/shared/icome-expenses-accounting/operation-list/OperationList.tsx create mode 100644 src/shared/portal-modal-form/PortalModalForm.module.scss create mode 100644 src/shared/portal-modal-form/PortalModalForm.tsx create mode 100644 src/stories/InputToPortalModal.stories.ts create mode 100644 src/stories/OpearationDynamicList.stories.tsx create mode 100644 src/stories/OperationList.stories.tsx create mode 100644 src/stories/OperationListAddMore.stories.tsx diff --git a/src/homeworks/ts1/3_write.ts b/src/homeworks/ts1/3_write.ts index d94010070..f6bdf5c79 100644 --- a/src/homeworks/ts1/3_write.ts +++ b/src/homeworks/ts1/3_write.ts @@ -4,8 +4,7 @@ * Поэтому в идеале чтобы функции возвращали случайные данные, но в то же время не абракадабру. * В целом сделайте так, как вам будет удобно. * */ -import crypto from 'crypto'; -import { names, photos, nouns, adjectives } from './data'; +import { names, photos, nouns, adjectives, bankCategories, bankOperations } from './data'; type Category = { id: string; @@ -24,7 +23,7 @@ type Product = { category: Category; }; -type Operation = Cost | Profit; +export type Operation = Cost | Profit; type Cost = { id: string; @@ -49,30 +48,30 @@ type Profit = { const getRandomItemFromArray = (arr: T[]): T => arr[Math.floor(Math.random() * arr.length)]; const getRandomNumber = (min: number, max: number): number => Math.floor(Math.random() * (max - min + 1)) + min; const getRandomDescription = (nouns: string[], adjectives: string[]): string => { - const fourAdjectives = [...Array(4)].map(() => getRandomItemFromArray(adjectives)).join(' '); + const someAdjectives = [...Array(50)].map(() => getRandomItemFromArray(adjectives)).join(' '); const noun = getRandomItemFromArray(nouns); - return `${fourAdjectives} ${noun}`; + return `${someAdjectives} ${noun}`; }; -const getRandomId = crypto.randomUUID; +const getRandomId = () => `${getRandomNumber(1000, 9999)}-${getRandomNumber(1000, 9999)}`; const createRandomCategory = (): Category => ({ id: getRandomId(), - name: getRandomItemFromArray(names), + name: getRandomItemFromArray(bankCategories), photo: getRandomItemFromArray(photos), }); const createRandomCost = (createdAt: string): Cost => ({ id: getRandomId(), - name: getRandomItemFromArray(names), + name: getRandomItemFromArray(bankOperations), desc: getRandomDescription(nouns, adjectives), createdAt, - amount: getRandomNumber(100, 1000), + amount: getRandomNumber(-100, -1000), category: createRandomCategory(), type: 'Cost', }); const createRandomProfit = (createdAt: string): Profit => ({ id: getRandomId(), - name: getRandomItemFromArray(names), + name: getRandomItemFromArray(bankOperations), desc: getRandomDescription(nouns, adjectives), createdAt, amount: getRandomNumber(100, 1000), diff --git a/src/homeworks/ts1/data.ts b/src/homeworks/ts1/data.ts index 9bd520d32..fe3e440a7 100644 --- a/src/homeworks/ts1/data.ts +++ b/src/homeworks/ts1/data.ts @@ -1,3 +1,125 @@ +export const bankCategories = [ + 'Account Management', + 'Transactions', + 'Loan Services', + 'Credit Card Services', + 'Investment Services', + 'Online and Mobile Banking', + 'Customer Support', + 'Foreign Exchange Services', + 'Other Services', + 'Security Services', + 'Digital Banking', + 'Insurance Services', + 'Financial Planning', + 'Mortgage Services', + 'Business Banking Services', + 'ATM Services', +]; + +export const bankOperations = [ + 'Open Savings Account', + 'Close Savings Account', + 'Open Checking Account', + 'Close Checking Account', + 'Open Fixed Deposit Account', + 'Close Fixed Deposit Account', + 'Open Joint Account', + 'Close Joint Account', + 'Update Account Information', + 'Change Account Type', + 'Deposit Cash', + 'Withdraw Cash', + 'Transfer Funds', + 'Receive Funds', + 'Pay Bills', + 'Online Payment', + 'Recurring Payments', + 'Stop Payment Request', + 'Issue Checks', + 'Cancel Checks', + 'Apply for Personal Loan', + 'Apply for Home Loan', + 'Apply for Auto Loan', + 'Apply for Student Loan', + 'Pay Loan Installment', + 'Pay Loan Off Early', + 'Refinance Loan', + 'Loan Balance Inquiry', + 'Loan Statement Request', + 'Loan Insurance', + 'Apply for Credit Card', + 'Activate Credit Card', + 'Block Credit Card', + 'Unblock Credit Card', + 'Increase Credit Limit', + 'Decrease Credit Limit', + 'Pay Credit Card Bill', + 'Credit Card Balance Inquiry', + 'Report Lost Credit Card', + 'Credit Card Statement Request', + 'Open Investment Account', + 'Close Investment Account', + 'Buy Stocks', + 'Sell Stocks', + 'Buy Bonds', + 'Sell Bonds', + 'Mutual Fund Investment', + 'Withdraw Investment', + 'Investment Account Statement', + 'Investment Advisory Services', + 'Register for Online Banking', + 'Login to Online Banking', + 'Reset Online Banking Password', + 'Mobile Banking Registration', + 'Mobile Banking App Download', + 'Mobile Check Deposit', + 'Online Fund Transfer', + 'View Account Statements', + 'Set Up Alerts', + 'Online Bill Payment', + 'Account Inquiry', + 'Transaction Dispute', + 'Account Lock/Unlock', + 'Complaint Registration', + 'General Banking Information', + 'ATM Card Issues', + 'Branch Locator', + 'Update Contact Details', + 'Request for Document Copies', + 'Fraud Alert Reporting', + 'Currency Exchange', + 'Foreign Currency Account Opening', + 'International Fund Transfer', + 'International Check Deposit', + 'Foreign Currency Loan', + 'Forex Card Application', + 'Forex Card Reload', + 'Forex Card Balance Inquiry', + 'Currency Rate Inquiry', + 'Foreign Draft Issue', + 'Safe Deposit Box Rental', + 'Safe Deposit Box Access', + 'Notary Services', + 'Document Certification', + 'Tax Payment Services', + 'Utility Bill Payments', + 'Property Valuation Services', + 'Mortgage Services', + 'Wealth Management Services', + 'Retirement Planning Services', + 'Activate Two-Factor Authentication', + 'Deactivate Two-Factor Authentication', + 'Change PIN/Password', + 'Set Up Security Questions', + 'Biometric Authentication Setup', + 'Review Security Settings', + 'Security Alerts & Notifications', + 'Anti-Money Laundering Reports', + 'Suspicious Activity Monitoring', + 'Data Privacy Requests', +]; + export const names = [ 'Emma', 'Liam', diff --git a/src/shared/icome-expenses-accounting/operation-container/OperationContainer.module.scss b/src/shared/icome-expenses-accounting/operation-container/OperationContainer.module.scss index 08374062b..6a14ec792 100644 --- a/src/shared/icome-expenses-accounting/operation-container/OperationContainer.module.scss +++ b/src/shared/icome-expenses-accounting/operation-container/OperationContainer.module.scss @@ -3,6 +3,5 @@ border: 1px solid #ddd; border-radius: 5px; padding: 15px; - margin-bottom: 20px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } diff --git a/src/shared/icome-expenses-accounting/operation-dynamic-list/OperationDynamicList.module.scss b/src/shared/icome-expenses-accounting/operation-dynamic-list/OperationDynamicList.module.scss new file mode 100644 index 000000000..0994f9c57 --- /dev/null +++ b/src/shared/icome-expenses-accounting/operation-dynamic-list/OperationDynamicList.module.scss @@ -0,0 +1,5 @@ +.operation-dynamic-list { + display: flex; + flex-direction: column; + gap: 15px; +} \ No newline at end of file diff --git a/src/shared/icome-expenses-accounting/operation-dynamic-list/OperationDynamicList.tsx b/src/shared/icome-expenses-accounting/operation-dynamic-list/OperationDynamicList.tsx new file mode 100644 index 000000000..a724198a4 --- /dev/null +++ b/src/shared/icome-expenses-accounting/operation-dynamic-list/OperationDynamicList.tsx @@ -0,0 +1,26 @@ +import React, { useCallback, useRef, useState } from 'react'; +import { OperationList } from '../operation-list/OperationList'; +import { createRandomOperation, Operation } from '../../../homeworks/ts1/3_write'; +import s from './OperationDynamicList.module.scss'; + +const createOpearation = () => createRandomOperation(new Date().toLocaleDateString()); +const createBatchOperations = (nr: number) => Array.from(Array(nr), () => createOpearation()); + +export const OperationDynamicList = () => { + const [operations, setOperations] = useState(createBatchOperations(5)); + const addMoreOperations = () => setOperations((prev) => [...prev, ...createBatchOperations(5)]); + const observer = useRef(null); + + const lastOperationRef = useCallback((node: HTMLDivElement) => { + observer.current && observer.current.disconnect(); + observer.current = new IntersectionObserver((items) => items[0].isIntersecting && addMoreOperations()); + node && observer.current.observe(node); + }, []); + + return ( +
+ +
+
+ ); +}; diff --git a/src/shared/icome-expenses-accounting/operation-list-add-more/OperationListAddMore.module.scss b/src/shared/icome-expenses-accounting/operation-list-add-more/OperationListAddMore.module.scss new file mode 100644 index 000000000..da70d5209 --- /dev/null +++ b/src/shared/icome-expenses-accounting/operation-list-add-more/OperationListAddMore.module.scss @@ -0,0 +1,5 @@ +.container { + display: flex; + flex-direction: column; + gap: 20px; +} \ No newline at end of file diff --git a/src/shared/icome-expenses-accounting/operation-list-add-more/OperationListAddMore.tsx b/src/shared/icome-expenses-accounting/operation-list-add-more/OperationListAddMore.tsx new file mode 100644 index 000000000..885108573 --- /dev/null +++ b/src/shared/icome-expenses-accounting/operation-list-add-more/OperationListAddMore.tsx @@ -0,0 +1,20 @@ +import React, { useState } from 'react'; +import { OperationList } from '../operation-list/OperationList'; +import { Button } from '../../button/Button'; +import { createRandomOperation, Operation } from '../../../homeworks/ts1/3_write'; +import s from './OperationListAddMore.module.scss'; + +const createOpearation = () => createRandomOperation(new Date().toLocaleDateString()); +const createBatchOperations = (nr: number) => Array.from(Array(nr), () => createOpearation()); + +export const OperationListAddMore = () => { + const [operations, setOperations] = useState(createBatchOperations(2)); + const addMoreOperations = () => setOperations((prev) => [...prev, ...createBatchOperations(2)]); + + return ( +
+ + +
+ ); +}; diff --git a/src/shared/icome-expenses-accounting/operation-list/OperationList.module.scss b/src/shared/icome-expenses-accounting/operation-list/OperationList.module.scss new file mode 100644 index 000000000..bf390d6dd --- /dev/null +++ b/src/shared/icome-expenses-accounting/operation-list/OperationList.module.scss @@ -0,0 +1,5 @@ +.operation-list { + display: flex; + flex-direction: column; + gap: 15px; +} \ No newline at end of file diff --git a/src/shared/icome-expenses-accounting/operation-list/OperationList.tsx b/src/shared/icome-expenses-accounting/operation-list/OperationList.tsx new file mode 100644 index 000000000..c89d1aea6 --- /dev/null +++ b/src/shared/icome-expenses-accounting/operation-list/OperationList.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { OperationDetail } from '../operation-detail/OperationDetail'; +import { Operation as OperationProps } from '../../../homeworks/ts1/3_write'; +import s from './OperationList.module.scss'; + +type OperationItemProps = Omit; + +const OperationItem = (data: OperationItemProps) => ( + +); + +type OperationListProps = { + items: OperationProps[]; +}; + +export const OperationList = ({ items }: OperationListProps) => ( +
+ {items.map((item) => { + const { id, ...rest } = item; + return ; + })} +
+); diff --git a/src/shared/icome-expenses-accounting/operation/Operation.module.scss b/src/shared/icome-expenses-accounting/operation/Operation.module.scss index a58796d24..c2137bcfa 100644 --- a/src/shared/icome-expenses-accounting/operation/Operation.module.scss +++ b/src/shared/icome-expenses-accounting/operation/Operation.module.scss @@ -19,3 +19,7 @@ font-weight: bold; margin-top: 10px; } + +.cost { + color: crimson; +} diff --git a/src/shared/icome-expenses-accounting/operation/Operation.tsx b/src/shared/icome-expenses-accounting/operation/Operation.tsx index 522fe000e..f5ee24653 100644 --- a/src/shared/icome-expenses-accounting/operation/Operation.tsx +++ b/src/shared/icome-expenses-accounting/operation/Operation.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import cn from 'clsx'; import s from './Operation.module.scss'; export type OperationProps = { @@ -9,7 +10,7 @@ export type OperationProps = { export const Operation = (data: OperationProps) => ( <> -
{data.amount}
+
{data.amount}
{data.category}
{data.title}
diff --git a/src/shared/modal-form/ModalForm.tsx b/src/shared/modal-form/ModalForm.tsx index 2f5171ded..b8ec9c772 100644 --- a/src/shared/modal-form/ModalForm.tsx +++ b/src/shared/modal-form/ModalForm.tsx @@ -7,8 +7,12 @@ type ModalFormProps = { onClose?: (event: MouseEvent) => void; }; -export const ModalForm = ({ visible = true, onClose, children }: ModalFormProps) => - !!visible && ( +export const ModalForm = ({ visible = true, onClose, children }: ModalFormProps) => { + if (!visible) { + return null; + } + + return (
@@ -18,3 +22,4 @@ export const ModalForm = ({ visible = true, onClose, children }: ModalFormProps)
); +}; diff --git a/src/shared/portal-modal-form/PortalModalForm.module.scss b/src/shared/portal-modal-form/PortalModalForm.module.scss new file mode 100644 index 000000000..d71cf65a2 --- /dev/null +++ b/src/shared/portal-modal-form/PortalModalForm.module.scss @@ -0,0 +1,10 @@ +.portal-modal { + display: flex; + flex-direction: column; + gap: 10px; + align-items: center; + + input { + align-self: stretch; + } +} diff --git a/src/shared/portal-modal-form/PortalModalForm.tsx b/src/shared/portal-modal-form/PortalModalForm.tsx new file mode 100644 index 000000000..2feeb0eae --- /dev/null +++ b/src/shared/portal-modal-form/PortalModalForm.tsx @@ -0,0 +1,28 @@ +import React, { useState, FormEvent } from 'react'; +import { createPortal } from 'react-dom'; +import { Button } from '../button/Button'; +import { TextInput } from '../text-input/TextInput'; +import { ModalForm } from '../modal-form/ModalForm'; +import s from './PortalModalForm.module.scss'; + +export const PortalModalForm = () => { + const [inputValue, setInputValue] = useState(''); + const [isModalFormVisible, setIsModalFormVisible] = useState(false); + + const openModalForm = () => setIsModalFormVisible(true); + const closeModalForm = () => setIsModalFormVisible(false); + const saveInputValue = (event: FormEvent) => setInputValue(event.currentTarget.value); + + return ( +
+ + + {createPortal( + +

{inputValue}

+
, + document.body + )} +
+ ); +}; diff --git a/src/stories/InputToModal.stories.ts b/src/stories/InputToModal.stories.ts index 316fe5eb7..b44835707 100644 --- a/src/stories/InputToModal.stories.ts +++ b/src/stories/InputToModal.stories.ts @@ -4,7 +4,7 @@ import { StatefullModalForm } from '../shared/statefull-modal-form/StatefullModa const meta: Meta = { component: StatefullModalForm, - title: 'Общее задание/Состояние модального окна', + title: 'Общее задание/Модальное окно/С состоянием', tags: ['autodocs'], parameters: { docs: { diff --git a/src/stories/InputToPortalModal.stories.ts b/src/stories/InputToPortalModal.stories.ts new file mode 100644 index 000000000..acc9f8b36 --- /dev/null +++ b/src/stories/InputToPortalModal.stories.ts @@ -0,0 +1,13 @@ +import type { Meta } from '@storybook/react'; + +import { PortalModalForm } from '../shared/portal-modal-form/PortalModalForm'; + +const meta: Meta = { + component: PortalModalForm, + title: 'Общее задание/Модальное окно/С порталом', + tags: ['autodocs'], +}; + +export default meta; + +export const Test = {}; diff --git a/src/stories/ModalForm.stories.tsx b/src/stories/ModalForm.stories.tsx index e4d70d61d..899486b85 100644 --- a/src/stories/ModalForm.stories.tsx +++ b/src/stories/ModalForm.stories.tsx @@ -13,7 +13,7 @@ const SomeContent = () => ( const meta: Meta = { component: ModalForm, - title: 'Общее задание/Модальное окно', + title: 'Общее задание/Модальное окно/Модальное окно', tags: ['autodocs'], argTypes: { children: { diff --git a/src/stories/OpearationDynamicList.stories.tsx b/src/stories/OpearationDynamicList.stories.tsx new file mode 100644 index 000000000..9bdf4e500 --- /dev/null +++ b/src/stories/OpearationDynamicList.stories.tsx @@ -0,0 +1,13 @@ +import type { Meta } from '@storybook/react'; + +import { OperationDynamicList } from '../shared/icome-expenses-accounting/operation-dynamic-list/OperationDynamicList'; + +const meta: Meta = { + component: OperationDynamicList, + title: 'Учет доходов-расходов/Список операций/Динамический с автодобавлением', + tags: ['autodocs'], +}; + +export default meta; + +export const Test = {}; diff --git a/src/stories/OperationDetail.stories.tsx b/src/stories/OperationDetail.stories.tsx index b2ade689a..7d1fc2782 100644 --- a/src/stories/OperationDetail.stories.tsx +++ b/src/stories/OperationDetail.stories.tsx @@ -4,7 +4,7 @@ import { OperationDetail } from '../shared/icome-expenses-accounting/operation-d const meta: Meta = { component: OperationDetail, - title: 'Учет доходов-расходов/Полная операция', + title: 'Учет доходов-расходов/Операция/Полная', tags: ['autodocs'], }; diff --git a/src/stories/OperationList.stories.tsx b/src/stories/OperationList.stories.tsx new file mode 100644 index 000000000..9599615c3 --- /dev/null +++ b/src/stories/OperationList.stories.tsx @@ -0,0 +1,29 @@ +import type { Meta } from '@storybook/react'; + +import { OperationList } from '../shared/icome-expenses-accounting/operation-list/OperationList'; +import { createRandomOperation } from '../homeworks/ts1/3_write'; + +const meta: Meta = { + component: OperationList, + title: 'Учет доходов-расходов/Список операций/Простой', + tags: ['autodocs'], +}; + +export default meta; + +export const Test = { + args: { + items: [ + createRandomOperation(new Date().toLocaleDateString()), + createRandomOperation(new Date().toLocaleDateString()), + createRandomOperation(new Date().toLocaleDateString()), + createRandomOperation(new Date().toLocaleDateString()), + createRandomOperation(new Date().toLocaleDateString()), + createRandomOperation(new Date().toLocaleDateString()), + createRandomOperation(new Date().toLocaleDateString()), + createRandomOperation(new Date().toLocaleDateString()), + createRandomOperation(new Date().toLocaleDateString()), + createRandomOperation(new Date().toLocaleDateString()), + ], + }, +}; diff --git a/src/stories/OperationListAddMore.stories.tsx b/src/stories/OperationListAddMore.stories.tsx new file mode 100644 index 000000000..a7f41d485 --- /dev/null +++ b/src/stories/OperationListAddMore.stories.tsx @@ -0,0 +1,13 @@ +import type { Meta } from '@storybook/react'; + +import { OperationListAddMore } from '../shared/icome-expenses-accounting/operation-list-add-more/OperationListAddMore'; + +const meta: Meta = { + component: OperationListAddMore, + title: 'Учет доходов-расходов/Список операций/Динамический с кнопкой', + tags: ['autodocs'], +}; + +export default meta; + +export const Test = {}; diff --git a/src/stories/OperationSummary.stories.tsx b/src/stories/OperationSummary.stories.tsx index d3bd83b18..33aadc38c 100644 --- a/src/stories/OperationSummary.stories.tsx +++ b/src/stories/OperationSummary.stories.tsx @@ -4,7 +4,7 @@ import { OperationSummary } from '../shared/icome-expenses-accounting/operation- const meta: Meta = { component: OperationSummary, - title: 'Учет доходов-расходов/Краткая операция', + title: 'Учет доходов-расходов/Операция/Краткая', tags: ['autodocs'], }; From ab4b02bb9c50d4c5ad0343463c18a09153719d9e Mon Sep 17 00:00:00 2001 From: Igor Aralov Date: Thu, 5 Dec 2024 21:55:42 +0300 Subject: [PATCH 18/27] Fix syntax error --- src/shared/modal-form/ModalForm.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/shared/modal-form/ModalForm.tsx b/src/shared/modal-form/ModalForm.tsx index 0a2944ecf..b8ec9c772 100644 --- a/src/shared/modal-form/ModalForm.tsx +++ b/src/shared/modal-form/ModalForm.tsx @@ -22,3 +22,4 @@ export const ModalForm = ({ visible = true, onClose, children }: ModalFormProps)
); +}; From 067eeb4ea54526e2244d5907a8a012c54a0f33d6 Mon Sep 17 00:00:00 2001 From: Igor Aralov Date: Tue, 14 Jan 2025 00:11:54 +0300 Subject: [PATCH 19/27] =?UTF-8?q?[8]=20=D0=A0=D0=B5=D0=B0=D0=BB=D0=B8?= =?UTF-8?q?=D0=B7=D0=BE=D0=B2=D0=B0=D1=82=D1=8C=20=D1=81=D0=BB=D0=BE=D0=B6?= =?UTF-8?q?=D0=BD=D1=8B=D0=B9=20=D0=BA=D0=BE=D0=BC=D0=BF=D0=BE=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=D1=82=20=D0=A0=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7=D0=BE=D0=B2?= =?UTF-8?q?=D0=B0=D0=BD=20=D0=BA=D0=BE=D0=BC=D0=BF=D0=BE=D0=BD=D0=B5=D0=BD?= =?UTF-8?q?=D1=82=20"=D0=9F=D0=BE=D0=B4=D1=81=D0=BA=D0=B0=D0=B7=D0=BA?= =?UTF-8?q?=D0=B0"=20(Tooltip)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/shared/tooltip/Tooltip.module.scss | 20 ++++ src/shared/tooltip/Tooltip.tsx | 120 ++++++++++++++++++++++ src/stories/TooltipInlineText.stories.tsx | 32 ++++++ src/stories/TooltipOnButton.stories.tsx | 20 ++++ 4 files changed, 192 insertions(+) create mode 100644 src/shared/tooltip/Tooltip.module.scss create mode 100644 src/shared/tooltip/Tooltip.tsx create mode 100644 src/stories/TooltipInlineText.stories.tsx create mode 100644 src/stories/TooltipOnButton.stories.tsx diff --git a/src/shared/tooltip/Tooltip.module.scss b/src/shared/tooltip/Tooltip.module.scss new file mode 100644 index 000000000..289c0abe7 --- /dev/null +++ b/src/shared/tooltip/Tooltip.module.scss @@ -0,0 +1,20 @@ +.tooltip-target { + display: inline-block; + position: relative; +} + +.tooltip-content { + position: absolute; + padding: 5px; + border-radius: 5px; + background-color: black; + color: whitesmoke; + opacity: 1; + transition-duration: var(--tooltip-animation-ms, 1000ms); + transition-property: opacity; + transition-timing-function: ease; +} + +.fade-out { + opacity: 0; +} diff --git a/src/shared/tooltip/Tooltip.tsx b/src/shared/tooltip/Tooltip.tsx new file mode 100644 index 000000000..fa186e3b0 --- /dev/null +++ b/src/shared/tooltip/Tooltip.tsx @@ -0,0 +1,120 @@ +import React, { useState, useRef, useLayoutEffect, ReactNode } from 'react'; +import { createPortal } from 'react-dom'; +import cn from 'clsx'; +import s from './Tooltip.module.scss'; + +type Coords = { + top: number; + left: number; +}; + +type CoordProps = { + targetRect: DOMRect; + tooltipRect: DOMRect; + offset: number; +}; + +type Position = 'top' | 'bottom' | 'left' | 'right'; + +type PositionMap = Record Coords>; + +const getCenterCoord = (primary: number, secondary: number) => (primary - secondary) / 2; + +const YLeft = (primary: DOMRect, secondary: DOMRect) => primary.left + getCenterCoord(primary.width, secondary.width); +const XTop = (primary: DOMRect, secondary: DOMRect) => primary.top + getCenterCoord(primary.height, secondary.height); + +const positionMap: PositionMap = { + top: ({ targetRect, tooltipRect, offset }) => ({ + top: targetRect.top - tooltipRect.height - offset, + left: YLeft(targetRect, tooltipRect), + }), + bottom: ({ targetRect, tooltipRect, offset }) => ({ + top: targetRect.bottom + offset, + left: YLeft(targetRect, tooltipRect), + }), + left: ({ targetRect, tooltipRect, offset }) => ({ + top: XTop(targetRect, tooltipRect), + left: targetRect.left - (tooltipRect.width + offset), + }), + + right: ({ targetRect, tooltipRect, offset }) => ({ + top: XTop(targetRect, tooltipRect), + left: targetRect.left + (targetRect.width + offset), + }), +}; + +type TooltipProps = { + children: ReactNode; + content: ReactNode; + duration?: number; + position?: Position; +}; + +export const Tooltip = ({ children, content, duration = 1000, position = 'bottom' }: TooltipProps) => { + const [visible, setVisible] = useState(false); + const [mounted, setMounted] = useState(false); + const [coords, setCoords] = useState({ top: 0, left: 0 }); + const tooltipRef = useRef(null); + const targetRef = useRef(null); + const timerRef = useRef(null); + const mountTimerRef = useRef(null); + + const mountTimer = 10; + + const clearTimeouts = () => { + timerRef.current && clearTimeout(timerRef.current); + mountTimerRef.current && clearTimeout(mountTimerRef.current); + }; + + const handleMouseEnter = () => { + clearTimeouts(); + setMounted(true); + mountTimerRef.current = setTimeout(() => setVisible(true), mountTimer); + }; + + const handleMouseLeave = () => { + setVisible(false); + timerRef.current = setTimeout(() => setMounted(false), duration + mountTimer); + }; + + useLayoutEffect(() => { + const target = targetRef.current; + const tooltip = tooltipRef.current; + + if (!target || !tooltip) return; + + tooltip.style.setProperty('--tooltip-animation-ms', `${duration + mountTimer}ms`); + + if (mounted) { + const targetRect = target.getBoundingClientRect(); + const tooltipRect = tooltip.getBoundingClientRect(); + console.log(targetRect); + const calcPosition = positionMap[position]; + setCoords(calcPosition({ targetRect, tooltipRect, offset: 5 })); + } + + return () => { + clearTimeouts(); + }; + }, [mounted]); + + return ( + <> + + {children} + + {mounted && + createPortal( +
+ {content} +
, + document.body + )} + + ); +}; diff --git a/src/stories/TooltipInlineText.stories.tsx b/src/stories/TooltipInlineText.stories.tsx new file mode 100644 index 000000000..fc88a0f38 --- /dev/null +++ b/src/stories/TooltipInlineText.stories.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import type { Meta } from '@storybook/react'; + +import { Tooltip } from '../shared/tooltip/Tooltip'; + +type TooltipInlineTextProps = { + content: React.ReactNode; +}; + +const TooltipInlineText = ({ content }: TooltipInlineTextProps) => ( +

+ {'Lorem ipsum dolor sit amet '} + + consectetur + + {' adipisicing elit.'} +

+); + +const meta: Meta = { + component: TooltipInlineText, + title: 'Сложные компоненты/Подсказка/В тексте', + tags: ['autodocs'], +}; + +export default meta; + +export const Test = { + args: { + content: 'Плавно всплывающая подсказка', + }, +}; diff --git a/src/stories/TooltipOnButton.stories.tsx b/src/stories/TooltipOnButton.stories.tsx new file mode 100644 index 000000000..ff59c693b --- /dev/null +++ b/src/stories/TooltipOnButton.stories.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import type { Meta } from '@storybook/react'; + +import { Tooltip } from '../shared/tooltip/Tooltip'; +import { Button } from '../shared/button/Button'; + +const meta: Meta = { + component: Tooltip, + title: 'Сложные компоненты/Подсказка/На кнопке', + tags: ['autodocs'], +}; + +export default meta; + +export const Test = { + args: { + children: , + content: 'Плавно всплывающая подсказка', + }, +}; From edc27217b4a6833398b0dd4b671e6b5f8ca7c127 Mon Sep 17 00:00:00 2001 From: Igor Aralov Date: Tue, 14 Jan 2025 00:18:44 +0300 Subject: [PATCH 20/27] Remove console.log --- src/shared/tooltip/Tooltip.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/shared/tooltip/Tooltip.tsx b/src/shared/tooltip/Tooltip.tsx index fa186e3b0..121c05a1a 100644 --- a/src/shared/tooltip/Tooltip.tsx +++ b/src/shared/tooltip/Tooltip.tsx @@ -88,7 +88,6 @@ export const Tooltip = ({ children, content, duration = 1000, position = 'bottom if (mounted) { const targetRect = target.getBoundingClientRect(); const tooltipRect = tooltip.getBoundingClientRect(); - console.log(targetRect); const calcPosition = positionMap[position]; setCoords(calcPosition({ targetRect, tooltipRect, offset: 5 })); } From 535f47b0ef4cca86a08cb63b49bd1261791b1c8b Mon Sep 17 00:00:00 2001 From: Igor Aralov Date: Sat, 18 Jan 2025 18:32:47 +0300 Subject: [PATCH 21/27] Fix positioning issues Add stories to storybook --- .../TooltipButtons.module.scss | 5 +++ src/shared/tooltip-buttons/TooltipButtons.tsx | 32 +++++++++++++++++++ src/shared/tooltip/Tooltip.module.scss | 1 + src/shared/tooltip/Tooltip.tsx | 26 ++++++++------- src/stories/TooltipOnButtons.stories.ts | 12 +++++++ ....tsx => TooltipWithProperties.stories.tsx} | 8 +++-- 6 files changed, 69 insertions(+), 15 deletions(-) create mode 100644 src/shared/tooltip-buttons/TooltipButtons.module.scss create mode 100644 src/shared/tooltip-buttons/TooltipButtons.tsx create mode 100644 src/stories/TooltipOnButtons.stories.ts rename src/stories/{TooltipOnButton.stories.tsx => TooltipWithProperties.stories.tsx} (70%) diff --git a/src/shared/tooltip-buttons/TooltipButtons.module.scss b/src/shared/tooltip-buttons/TooltipButtons.module.scss new file mode 100644 index 000000000..b20682ff9 --- /dev/null +++ b/src/shared/tooltip-buttons/TooltipButtons.module.scss @@ -0,0 +1,5 @@ +.tooltip-buttons { + display: flex; + gap: 20px; + justify-content: space-between; +} \ No newline at end of file diff --git a/src/shared/tooltip-buttons/TooltipButtons.tsx b/src/shared/tooltip-buttons/TooltipButtons.tsx new file mode 100644 index 000000000..37d399c0d --- /dev/null +++ b/src/shared/tooltip-buttons/TooltipButtons.tsx @@ -0,0 +1,32 @@ +import React, { ReactNode } from 'react'; +import { Position, Tooltip } from '../tooltip/Tooltip'; +import { Button } from '../button/Button'; +import s from './TooltipButtons.module.scss'; + +type PredefinedTooltipProps = { + children: ReactNode; + position: Position; +}; + +const PredefinedTooltip = ({ children, position }: PredefinedTooltipProps) => ( + + {children} + +); + +export const TooltipButtons = () => ( +
+ + + + + + + + + + + + +
+); diff --git a/src/shared/tooltip/Tooltip.module.scss b/src/shared/tooltip/Tooltip.module.scss index 289c0abe7..ece4fb9dd 100644 --- a/src/shared/tooltip/Tooltip.module.scss +++ b/src/shared/tooltip/Tooltip.module.scss @@ -4,6 +4,7 @@ } .tooltip-content { + z-index: 1; position: absolute; padding: 5px; border-radius: 5px; diff --git a/src/shared/tooltip/Tooltip.tsx b/src/shared/tooltip/Tooltip.tsx index 121c05a1a..d91366f9e 100644 --- a/src/shared/tooltip/Tooltip.tsx +++ b/src/shared/tooltip/Tooltip.tsx @@ -14,32 +14,35 @@ type CoordProps = { offset: number; }; -type Position = 'top' | 'bottom' | 'left' | 'right'; +export type Position = 'top' | 'bottom' | 'left' | 'right'; type PositionMap = Record Coords>; const getCenterCoord = (primary: number, secondary: number) => (primary - secondary) / 2; -const YLeft = (primary: DOMRect, secondary: DOMRect) => primary.left + getCenterCoord(primary.width, secondary.width); -const XTop = (primary: DOMRect, secondary: DOMRect) => primary.top + getCenterCoord(primary.height, secondary.height); +const YLeft = (primary: DOMRect, secondary: DOMRect) => + primary.left + window.scrollX + getCenterCoord(primary.width, secondary.width); +const XTop = (primary: DOMRect, secondary: DOMRect) => + primary.top + window.scrollY + getCenterCoord(primary.height, secondary.height); const positionMap: PositionMap = { top: ({ targetRect, tooltipRect, offset }) => ({ - top: targetRect.top - tooltipRect.height - offset, - left: YLeft(targetRect, tooltipRect), + top: targetRect.top + window.scrollY - tooltipRect.height - offset, + left: targetRect.left + window.scrollX + getCenterCoord(targetRect.width, tooltipRect.width), }), + bottom: ({ targetRect, tooltipRect, offset }) => ({ - top: targetRect.bottom + offset, + top: targetRect.bottom + window.scrollY + offset, left: YLeft(targetRect, tooltipRect), }), left: ({ targetRect, tooltipRect, offset }) => ({ top: XTop(targetRect, tooltipRect), - left: targetRect.left - (tooltipRect.width + offset), + left: targetRect.left + window.scrollX - tooltipRect.width - offset, }), right: ({ targetRect, tooltipRect, offset }) => ({ top: XTop(targetRect, tooltipRect), - left: targetRect.left + (targetRect.width + offset), + left: targetRect.left + window.scrollX + targetRect.width + offset, }), }; @@ -59,7 +62,7 @@ export const Tooltip = ({ children, content, duration = 1000, position = 'bottom const timerRef = useRef(null); const mountTimerRef = useRef(null); - const mountTimer = 10; + const mountTimer = 50; const clearTimeouts = () => { timerRef.current && clearTimeout(timerRef.current); @@ -83,12 +86,11 @@ export const Tooltip = ({ children, content, duration = 1000, position = 'bottom if (!target || !tooltip) return; - tooltip.style.setProperty('--tooltip-animation-ms', `${duration + mountTimer}ms`); - if (mounted) { + tooltipRef.current?.style.setProperty('--tooltip-animation-ms', `${duration + mountTimer}ms`); const targetRect = target.getBoundingClientRect(); const tooltipRect = tooltip.getBoundingClientRect(); - const calcPosition = positionMap[position]; + const calcPosition = positionMap[position] ?? positionMap['bottom']; setCoords(calcPosition({ targetRect, tooltipRect, offset: 5 })); } diff --git a/src/stories/TooltipOnButtons.stories.ts b/src/stories/TooltipOnButtons.stories.ts new file mode 100644 index 000000000..fa36d0e9e --- /dev/null +++ b/src/stories/TooltipOnButtons.stories.ts @@ -0,0 +1,12 @@ +import type { Meta } from '@storybook/react'; +import { TooltipButtons } from '../shared/tooltip-buttons/TooltipButtons'; + +const meta: Meta = { + component: TooltipButtons, + title: 'Сложные компоненты/Подсказка/Позиционирование', + tags: ['autodocs'], +}; + +export default meta; + +export const Test = {}; diff --git a/src/stories/TooltipOnButton.stories.tsx b/src/stories/TooltipWithProperties.stories.tsx similarity index 70% rename from src/stories/TooltipOnButton.stories.tsx rename to src/stories/TooltipWithProperties.stories.tsx index ff59c693b..1a628cc08 100644 --- a/src/stories/TooltipOnButton.stories.tsx +++ b/src/stories/TooltipWithProperties.stories.tsx @@ -6,7 +6,7 @@ import { Button } from '../shared/button/Button'; const meta: Meta = { component: Tooltip, - title: 'Сложные компоненты/Подсказка/На кнопке', + title: 'Сложные компоненты/Подсказка/С настройками', tags: ['autodocs'], }; @@ -14,7 +14,9 @@ export default meta; export const Test = { args: { - children: , - content: 'Плавно всплывающая подсказка', + content: 'Подсказка', + duration: 1000, + position: 'top', + children: , }, }; From bab705b8fb017130b39e6a162b8044974881d071 Mon Sep 17 00:00:00 2001 From: Igor Aralov Date: Sun, 19 Jan 2025 16:04:32 +0300 Subject: [PATCH 22/27] Add hover effect to button --- src/shared/button/Button.modules.scss | 14 ++++++++++---- src/shared/button/Button.tsx | 3 +-- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/shared/button/Button.modules.scss b/src/shared/button/Button.modules.scss index b476ee8aa..367b092c6 100644 --- a/src/shared/button/Button.modules.scss +++ b/src/shared/button/Button.modules.scss @@ -6,9 +6,15 @@ border: none; border-radius: 5px; cursor: pointer; -} -.disabled { - cursor: not-allowed; - opacity: 0.6; + &:hover { + &:not([disabled]) { + background-color: #0056b3; + } + } + + &:disabled { + cursor: not-allowed; + opacity: 0.6; + } } diff --git a/src/shared/button/Button.tsx b/src/shared/button/Button.tsx index 41bd4eef8..57ac8ecbb 100644 --- a/src/shared/button/Button.tsx +++ b/src/shared/button/Button.tsx @@ -1,5 +1,4 @@ import React, { ReactNode, MouseEvent } from 'react'; -import cn from 'clsx'; import s from './Button.modules.scss'; type ButtonProps = { @@ -9,7 +8,7 @@ type ButtonProps = { }; export const Button = ({ children, onClick, disabled = false }: ButtonProps) => ( - ); From ac21245adcf9f645a2cd3c5cb8e1805fb9885c56 Mon Sep 17 00:00:00 2001 From: Igor Aralov Date: Sun, 19 Jan 2025 16:06:32 +0300 Subject: [PATCH 23/27] =?UTF-8?q?[9]=20=D0=A0=D0=B5=D0=B0=D0=BB=D0=B8?= =?UTF-8?q?=D0=B7=D0=BE=D0=B2=D0=B0=D1=82=D1=8C=20=D1=81=D0=BB=D0=BE=D0=B6?= =?UTF-8?q?=D0=BD=D1=8B=D0=B9=20=D0=BA=D0=BE=D0=BC=D0=BF=D0=BE=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=D1=82=20=D0=A0=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7=D0=BE=D0=B2?= =?UTF-8?q?=D0=B0=D0=BD=20=D0=BA=D0=BE=D0=BC=D0=BF=D0=BE=D0=BD=D0=B5=D0=BD?= =?UTF-8?q?=D1=82=20"=D0=A1=D0=B2=D0=BE=D1=80=D0=B0=D1=87=D0=B8=D0=B2?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D0=B5"=20(Collapse)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/shared/collapse/Collapse.module.scss | 27 +++++++++++++ src/shared/collapse/Collapse.tsx | 50 ++++++++++++++++++++++++ src/stories/Collapse.stories.tsx | 29 ++++++++++++++ 3 files changed, 106 insertions(+) create mode 100644 src/shared/collapse/Collapse.module.scss create mode 100644 src/shared/collapse/Collapse.tsx create mode 100644 src/stories/Collapse.stories.tsx diff --git a/src/shared/collapse/Collapse.module.scss b/src/shared/collapse/Collapse.module.scss new file mode 100644 index 000000000..c12dfaa73 --- /dev/null +++ b/src/shared/collapse/Collapse.module.scss @@ -0,0 +1,27 @@ +.collapse-container { + border: 1px solid #ccc; + border-radius: 4px; + margin: 10px 0; + overflow: hidden; +} + +.collapse-button { + width: 100%; + background-color: #007bff; + color: white; + padding: 10px; + border: none; + cursor: pointer; + text-align: left; + font-size: 1em; + + &:hover { + background-color: #0056b3; + } +} + +.collapse-content { + padding: 10px; + background-color: #f9f9f9; + transition: height 0.3s ease; +} diff --git a/src/shared/collapse/Collapse.tsx b/src/shared/collapse/Collapse.tsx new file mode 100644 index 000000000..a5b8a730f --- /dev/null +++ b/src/shared/collapse/Collapse.tsx @@ -0,0 +1,50 @@ +import React, { useState, useRef, useEffect, ReactNode } from 'react'; +import s from './Collapse.module.scss'; + +type CollapseProps = { + title: string; + children: ReactNode; +}; + +export const Collapse = ({ title, children }: CollapseProps) => { + const [isOpen, setIsOpen] = useState(false); + const [height, setHeight] = useState(0); + const contentRef = useRef(null); + + const toggleCollapse = () => { + setIsOpen(!isOpen); + }; + + useEffect(() => { + const resizeObserver = new ResizeObserver((entries) => + entries.forEach((entry) => setHeight(entry.borderBoxSize[0].blockSize)) + ); + + const currentContentRef = contentRef.current; + + currentContentRef && resizeObserver.observe(currentContentRef); + + return () => { + currentContentRef && resizeObserver.unobserve(currentContentRef); + }; + }, []); + + return ( +
+ +
+
+ {children} +
+
+
+ ); +}; diff --git a/src/stories/Collapse.stories.tsx b/src/stories/Collapse.stories.tsx new file mode 100644 index 000000000..e43b503a4 --- /dev/null +++ b/src/stories/Collapse.stories.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import type { Meta } from '@storybook/react'; + +import { Collapse } from '../shared/collapse/Collapse'; +import { Button } from '../shared/button/Button'; + +const meta: Meta = { + component: Collapse, + title: 'Сложные компоненты/Сворачивание', + tags: ['autodocs'], +}; + +export default meta; + +export const Test = { + args: { + title: 'Сворачиваемый контент', + children: ( + <> + +

+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Incidunt, perferendis! Non autem consectetur amet + sint at saepe veniam doloremque, culpa dolore corporis ad consequuntur temporibus? Magni laudantium eum dolor + tempora. +

+ + ), + }, +}; From 59412e391d57c99661e71dbe4ec8f2253abe63d9 Mon Sep 17 00:00:00 2001 From: Igor Aralov Date: Tue, 21 Jan 2025 01:42:56 +0300 Subject: [PATCH 24/27] =?UTF-8?q?[10]=20=D0=A4=D0=BE=D1=80=D0=BC=D1=8B=20?= =?UTF-8?q?=D0=A0=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7=D0=BE=D0=B2=D0=B0=D0=BD?= =?UTF-8?q?=D1=8B=20=D1=84=D0=BE=D1=80=D0=BC=D1=8B:=20=D0=9F=D1=80=D0=BE?= =?UTF-8?q?=D1=84=D0=B8=D0=BB=D1=8C,=20=D0=92=D1=85=D0=BE=D0=B4,=20=D0=A0?= =?UTF-8?q?=D0=B5=D0=B3=D0=B8=D1=81=D1=82=D1=80=D0=B0=D1=86=D0=B8=D1=8F,?= =?UTF-8?q?=20=D0=9E=D0=BF=D0=B5=D1=80=D0=B0=D1=86=D0=B8=D1=8F,=20=D0=A2?= =?UTF-8?q?=D0=BE=D0=B2=D0=B0=D1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 56 ++++++++++++- package.json | 5 +- src/pages/OperationForm/OperationForm.tsx | 65 +++++++++++++++ src/pages/OperationForm/operation-schema.ts | 12 +++ src/pages/ProductForm/ProductForm.tsx | 51 ++++++++++++ src/pages/ProductForm/product-schema.ts | 16 ++++ src/pages/ProfileForm/ProfileForm.module.scss | 5 ++ src/pages/ProfileForm/ProfileForm.tsx | 80 +++++++++++++++++++ src/pages/ProfileForm/profile-schema.ts | 23 ++++++ src/pages/SignInForm/SignInForm.tsx | 38 +++++++++ src/pages/SignInForm/signin-schema.ts | 8 ++ src/pages/SignUpForm/SignUpForm.tsx | 38 +++++++++ src/pages/SignUpForm/signup-schema.ts | 8 ++ src/shared/button/Button.tsx | 5 +- .../FormInputField/FormInputField.module.scss | 26 ++++++ .../forms/FormInputField/FormInputField.tsx | 32 ++++++++ .../FormSelectField.module.scss | 26 ++++++ .../forms/FormSelectField/FormSelectField.tsx | 37 +++++++++ .../FormTextareaField.module.scss | 27 +++++++ .../FormTextareaField/FormTextareaField.tsx | 26 ++++++ .../forms/RegularForm/RegularForm.module.scss | 14 ++++ src/shared/forms/RegularForm/RegularForm.tsx | 17 ++++ src/stories/OperationForm.stories.ts | 13 +++ src/stories/ProductForm.stories.ts | 13 +++ src/stories/ProfileForm.stories.ts | 13 +++ src/stories/SignInForm.stories.ts | 13 +++ src/stories/SignUpForm.stories.ts | 13 +++ 27 files changed, 676 insertions(+), 4 deletions(-) create mode 100644 src/pages/OperationForm/OperationForm.tsx create mode 100644 src/pages/OperationForm/operation-schema.ts create mode 100644 src/pages/ProductForm/ProductForm.tsx create mode 100644 src/pages/ProductForm/product-schema.ts create mode 100644 src/pages/ProfileForm/ProfileForm.module.scss create mode 100644 src/pages/ProfileForm/ProfileForm.tsx create mode 100644 src/pages/ProfileForm/profile-schema.ts create mode 100644 src/pages/SignInForm/SignInForm.tsx create mode 100644 src/pages/SignInForm/signin-schema.ts create mode 100644 src/pages/SignUpForm/SignUpForm.tsx create mode 100644 src/pages/SignUpForm/signup-schema.ts create mode 100644 src/shared/forms/FormInputField/FormInputField.module.scss create mode 100644 src/shared/forms/FormInputField/FormInputField.tsx create mode 100644 src/shared/forms/FormSelectField/FormSelectField.module.scss create mode 100644 src/shared/forms/FormSelectField/FormSelectField.tsx create mode 100644 src/shared/forms/FormTextareaField/FormTextareaField.module.scss create mode 100644 src/shared/forms/FormTextareaField/FormTextareaField.tsx create mode 100644 src/shared/forms/RegularForm/RegularForm.module.scss create mode 100644 src/shared/forms/RegularForm/RegularForm.tsx create mode 100644 src/stories/OperationForm.stories.ts create mode 100644 src/stories/ProductForm.stories.ts create mode 100644 src/stories/ProfileForm.stories.ts create mode 100644 src/stories/SignInForm.stories.ts create mode 100644 src/stories/SignUpForm.stories.ts diff --git a/package-lock.json b/package-lock.json index 5b1dfa392..b9fef52d2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,9 +9,12 @@ "version": "1.0.0", "license": "MIT", "dependencies": { + "@hookform/resolvers": "^3.10.0", "clsx": "^1.2.1", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "react-hook-form": "^7.54.2", + "zod": "^3.24.1" }, "devDependencies": { "@babel/core": "^7.22.1", @@ -2620,6 +2623,15 @@ "integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==", "dev": true }, + "node_modules/@hookform/resolvers": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.10.0.tgz", + "integrity": "sha512-79Dv+3mDF7i+2ajj7SkypSKHhl1cbln1OGavqrsF7p6mbUv11xpqpacPsGDCTRvCSjEEIez2ef1NveSVL3b0Ag==", + "license": "MIT", + "peerDependencies": { + "react-hook-form": "^7.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.10.7", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.10.7.tgz", @@ -19941,6 +19953,22 @@ "integrity": "sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg==", "dev": true }, + "node_modules/react-hook-form": { + "version": "7.54.2", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.54.2.tgz", + "integrity": "sha512-eHpAUgUjWbZocoQYUHposymRb4ZP6d0uwUnooL2uOybA9/3tPUvoAKqEWK1WaSiTxxOfTpffNZP7QwlnM3/gEg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -23542,6 +23570,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zod": { + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", + "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/zwitch": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", @@ -25246,6 +25283,12 @@ "integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==", "dev": true }, + "@hookform/resolvers": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.10.0.tgz", + "integrity": "sha512-79Dv+3mDF7i+2ajj7SkypSKHhl1cbln1OGavqrsF7p6mbUv11xpqpacPsGDCTRvCSjEEIez2ef1NveSVL3b0Ag==", + "requires": {} + }, "@humanwhocodes/config-array": { "version": "0.10.7", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.10.7.tgz", @@ -37986,6 +38029,12 @@ } } }, + "react-hook-form": { + "version": "7.54.2", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.54.2.tgz", + "integrity": "sha512-eHpAUgUjWbZocoQYUHposymRb4ZP6d0uwUnooL2uOybA9/3tPUvoAKqEWK1WaSiTxxOfTpffNZP7QwlnM3/gEg==", + "requires": {} + }, "react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -40641,6 +40690,11 @@ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true }, + "zod": { + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", + "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==" + }, "zwitch": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", diff --git a/package.json b/package.json index 492664d1f..d77f70944 100644 --- a/package.json +++ b/package.json @@ -64,8 +64,11 @@ "webpack-dev-server": "^4.15.0" }, "dependencies": { + "@hookform/resolvers": "^3.10.0", "clsx": "^1.2.1", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "react-hook-form": "^7.54.2", + "zod": "^3.24.1" } } diff --git a/src/pages/OperationForm/OperationForm.tsx b/src/pages/OperationForm/OperationForm.tsx new file mode 100644 index 000000000..a3153be53 --- /dev/null +++ b/src/pages/OperationForm/OperationForm.tsx @@ -0,0 +1,65 @@ +import React from 'react'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; + +import { RegularForm } from '../../shared/forms/RegularForm/RegularForm'; +import { Button } from '../../shared/button/Button'; +import { FormInputField } from '../../shared/forms/FormInputField/FormInputField'; +import { FormSelectField, SelectOptionProps } from '../../shared/forms/FormSelectField/FormSelectField'; + +import { OperationSchemaType, OperationSchema } from './operation-schema'; + +const costOperationOption: SelectOptionProps = { + text: 'Cost', + value: 'Cost', +}; + +const profitOperationOption: SelectOptionProps = { + text: 'Profit', + value: 'Profit', +}; + +export const OperationForm = () => { + const { + register, + handleSubmit, + formState: { errors }, + } = useForm({ + shouldUnregister: true, + resolver: zodResolver(OperationSchema), + }); + + const onSubmit = (data: OperationSchemaType) => { + console.log(data); + }; + + return ( + + + Тип операции + + + Название + + + Категория + + + Описание + + + Дата + + + Сумма + + + + + ); +}; diff --git a/src/pages/OperationForm/operation-schema.ts b/src/pages/OperationForm/operation-schema.ts new file mode 100644 index 000000000..b38f30be3 --- /dev/null +++ b/src/pages/OperationForm/operation-schema.ts @@ -0,0 +1,12 @@ +import { z } from 'zod'; + +export const OperationSchema = z.object({ + name: z.string().nonempty('Обязательное поле'), + desc: z.string().optional(), + createdAt: z.string().nonempty('Обязательное поле'), + amount: z.number({ invalid_type_error: 'Обязательное поле' }).positive('Сумма должна быть больше 0'), + category: z.string().nonempty('Обязательное поле'), + type: z.enum(['Cost', 'Profit']), +}); + +export type OperationSchemaType = z.infer; diff --git a/src/pages/ProductForm/ProductForm.tsx b/src/pages/ProductForm/ProductForm.tsx new file mode 100644 index 000000000..60a67a73b --- /dev/null +++ b/src/pages/ProductForm/ProductForm.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; + +import { RegularForm } from '../../shared/forms/RegularForm/RegularForm'; +import { Button } from '../../shared/button/Button'; +import { FormInputField } from '../../shared/forms/FormInputField/FormInputField'; + +import { ProductSchema, ProductSchemaType } from './product-schema'; + +export const ProductForm = () => { + const { + register, + handleSubmit, + formState: { errors }, + } = useForm({ + shouldUnregister: true, + resolver: zodResolver(ProductSchema), + }); + + const onSubmit = (data: ProductSchemaType) => { + console.log(data); + }; + + return ( + + + Название + + + Категория + + + Описание + + + Фото + + + Дата + + + Старая цена + + + Цена + + + + ); +}; diff --git a/src/pages/ProductForm/product-schema.ts b/src/pages/ProductForm/product-schema.ts new file mode 100644 index 000000000..ab217fa15 --- /dev/null +++ b/src/pages/ProductForm/product-schema.ts @@ -0,0 +1,16 @@ +import { z } from 'zod'; + +export const ProductSchema = z.object({ + name: z.string().nonempty('Обязательное поле'), + photo: z.string().url('Некорректный URL').nonempty('Обязательное поле'), + desc: z.string().optional(), + createdAt: z.string().nonempty('Обязательное поле'), + oldPrice: z + .number({ invalid_type_error: 'Некорректное значение, введите 0 если значение отсутстувет' }) + .min(0, 'Сумма должна быть 0 или больше') + .default(0), + price: z.number({ invalid_type_error: 'Обязательное поле' }).positive('Сумма должна быть больше 0'), + category: z.string().nonempty('Обязательное поле'), +}); + +export type ProductSchemaType = z.infer; diff --git a/src/pages/ProfileForm/ProfileForm.module.scss b/src/pages/ProfileForm/ProfileForm.module.scss new file mode 100644 index 000000000..168f49fd7 --- /dev/null +++ b/src/pages/ProfileForm/ProfileForm.module.scss @@ -0,0 +1,5 @@ +.container { + display: flex; + flex-direction: column; + gap: 10px; +} \ No newline at end of file diff --git a/src/pages/ProfileForm/ProfileForm.tsx b/src/pages/ProfileForm/ProfileForm.tsx new file mode 100644 index 000000000..4ae335d8e --- /dev/null +++ b/src/pages/ProfileForm/ProfileForm.tsx @@ -0,0 +1,80 @@ +import React from 'react'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; + +import { + ChangeProfileSchema, + ChangeProfileSchemaType, + ChangePasswordSchema, + ChangePasswordSchemaType, +} from './profile-schema'; +import { RegularForm } from '../../shared/forms/RegularForm/RegularForm'; +import { FormInputField } from '../../shared/forms/FormInputField/FormInputField'; +import { FormTextareaField } from '../../shared/forms/FormTextareaField/FormTextareaField'; +import { Button } from '../../shared/button/Button'; + +import s from './ProfileForm.module.scss'; + +const ChangeProfileForm = () => { + const { + register, + handleSubmit, + formState: { errors }, + } = useForm({ + shouldUnregister: true, + resolver: zodResolver(ChangeProfileSchema), + }); + + const onSubmit = (data: ChangeProfileSchemaType) => { + console.log(data); + }; + + return ( + + + Псевдоним + + + О себе + + + + ); +}; + +const ChangePasswordForm = () => { + const { + register, + handleSubmit, + formState: { errors }, + } = useForm({ + shouldUnregister: true, + resolver: zodResolver(ChangePasswordSchema), + }); + + const onSubmit = (data: ChangePasswordSchemaType) => { + console.log(data); + }; + + return ( + + + Пароль + + + Новый пароль + + + Повторите пароль + + + + ); +}; + +export const ProfileForm = () => ( +
+ + +
+); diff --git a/src/pages/ProfileForm/profile-schema.ts b/src/pages/ProfileForm/profile-schema.ts new file mode 100644 index 000000000..60c48f35c --- /dev/null +++ b/src/pages/ProfileForm/profile-schema.ts @@ -0,0 +1,23 @@ +import { z } from 'zod'; + +export const ChangeProfileSchema = z.object({ + name: z.string().nonempty('Обязательное поле'), + description: z.string(), +}); + +export type ChangeProfileSchemaType = z.infer; + +const passwordZodType = z.string().nonempty('Обязательное поле').min(6, 'Слишком короткий пароль'); + +export const ChangePasswordSchema = z + .object({ + password: passwordZodType, + newPassword: passwordZodType, + confirmPassword: passwordZodType, + }) + .refine((data) => data.newPassword === data.confirmPassword, { + message: 'Пароли не совпадают', + path: ['confirmPassword'], + }); + +export type ChangePasswordSchemaType = z.infer; diff --git a/src/pages/SignInForm/SignInForm.tsx b/src/pages/SignInForm/SignInForm.tsx new file mode 100644 index 000000000..08b60f2de --- /dev/null +++ b/src/pages/SignInForm/SignInForm.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; + +import { RegularForm } from '../../shared/forms/RegularForm/RegularForm'; +import { Button } from '../../shared/button/Button'; +import { FormInputField } from '../../shared/forms/FormInputField/FormInputField'; + +import { SignInSchema, SignInSchemaType } from './signin-schema'; + +export const SignInForm = () => { + const { + reset, + register, + handleSubmit, + formState: { errors }, + } = useForm({ + shouldUnregister: true, + resolver: zodResolver(SignInSchema), + }); + + const onSubmit = (data: SignInSchemaType) => { + console.log(data); + reset(); + }; + + return ( + + + Email + + + Пароль + + + + ); +}; diff --git a/src/pages/SignInForm/signin-schema.ts b/src/pages/SignInForm/signin-schema.ts new file mode 100644 index 000000000..def0fa9ee --- /dev/null +++ b/src/pages/SignInForm/signin-schema.ts @@ -0,0 +1,8 @@ +import { z } from 'zod'; + +export const SignInSchema = z.object({ + email: z.string().nonempty('Обязательное поле').email('Неправильный Email'), + password: z.string().nonempty('Обязательное поле').min(6, 'Слишком короткий пароль'), +}); + +export type SignInSchemaType = z.infer; diff --git a/src/pages/SignUpForm/SignUpForm.tsx b/src/pages/SignUpForm/SignUpForm.tsx new file mode 100644 index 000000000..d714f03eb --- /dev/null +++ b/src/pages/SignUpForm/SignUpForm.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; + +import { RegularForm } from '../../shared/forms/RegularForm/RegularForm'; +import { Button } from '../../shared/button/Button'; +import { FormInputField } from '../../shared/forms/FormInputField/FormInputField'; + +import { SignUpSchemaType, SignUpSchema } from './signup-schema'; + +export const SignUpForm = () => { + const { + reset, + register, + handleSubmit, + formState: { errors }, + } = useForm({ + shouldUnregister: true, + resolver: zodResolver(SignUpSchema), + }); + + const onSubmit = (data: SignUpSchemaType) => { + console.log(data); + reset(); + }; + + return ( + + + Email + + + Пароль + + + + ); +}; diff --git a/src/pages/SignUpForm/signup-schema.ts b/src/pages/SignUpForm/signup-schema.ts new file mode 100644 index 000000000..210942b9c --- /dev/null +++ b/src/pages/SignUpForm/signup-schema.ts @@ -0,0 +1,8 @@ +import { z } from 'zod'; + +export const SignUpSchema = z.object({ + email: z.string().nonempty('Обязательное поле').email('Неправильный Email'), + password: z.string().nonempty('Обязательное поле').min(6, 'Слишком короткий пароль'), +}); + +export type SignUpSchemaType = z.infer; diff --git a/src/shared/button/Button.tsx b/src/shared/button/Button.tsx index 57ac8ecbb..9125072ea 100644 --- a/src/shared/button/Button.tsx +++ b/src/shared/button/Button.tsx @@ -3,12 +3,13 @@ import s from './Button.modules.scss'; type ButtonProps = { children: ReactNode; + type?: 'submit' | 'reset' | 'button'; disabled?: boolean; onClick?: (event: MouseEvent) => void; }; -export const Button = ({ children, onClick, disabled = false }: ButtonProps) => ( - ); diff --git a/src/shared/forms/FormInputField/FormInputField.module.scss b/src/shared/forms/FormInputField/FormInputField.module.scss new file mode 100644 index 000000000..8f214f602 --- /dev/null +++ b/src/shared/forms/FormInputField/FormInputField.module.scss @@ -0,0 +1,26 @@ +.container { + display: flex; + flex-direction: column; +} + +.label { + display: block; + margin-bottom: 0.5rem; + font-weight: bold; +} + +.input { + padding: 0.5rem; + border: 1px solid #ccc; + border-radius: 4px; + + &.error { + border-color: red; + } +} + +.error-message { + color: red; + font-size: 0.875rem; + padding-top: 0.3rem; +} diff --git a/src/shared/forms/FormInputField/FormInputField.tsx b/src/shared/forms/FormInputField/FormInputField.tsx new file mode 100644 index 000000000..a8d4ecd3c --- /dev/null +++ b/src/shared/forms/FormInputField/FormInputField.tsx @@ -0,0 +1,32 @@ +import React, { ReactNode } from 'react'; +import { FieldError, UseFormRegister, Path, FieldValues } from 'react-hook-form'; +import cn from 'clsx'; +import s from './FormInputField.module.scss'; + +type FormInputFieldProps = { + children: ReactNode; + type: string; + register: UseFormRegister; + name: Path; + isNumber?: boolean; + errors?: FieldError; +}; + +export const FormInputField = ({ + children, + type, + register, + isNumber, + name, + errors, +}: FormInputFieldProps) => ( +
+ + + {errors && {errors.message}} +
+); diff --git a/src/shared/forms/FormSelectField/FormSelectField.module.scss b/src/shared/forms/FormSelectField/FormSelectField.module.scss new file mode 100644 index 000000000..2b15fe113 --- /dev/null +++ b/src/shared/forms/FormSelectField/FormSelectField.module.scss @@ -0,0 +1,26 @@ +.container { + display: flex; + flex-direction: column; +} + +.label { + display: block; + margin-bottom: 0.5rem; + font-weight: bold; +} + +.select { + padding: 0.5rem; + border: 1px solid #ccc; + border-radius: 4px; + + &.error { + border-color: red; + } +} + +.error-message { + color: red; + font-size: 0.875rem; + padding-top: 0.3rem; +} diff --git a/src/shared/forms/FormSelectField/FormSelectField.tsx b/src/shared/forms/FormSelectField/FormSelectField.tsx new file mode 100644 index 000000000..ef5184ae1 --- /dev/null +++ b/src/shared/forms/FormSelectField/FormSelectField.tsx @@ -0,0 +1,37 @@ +import React, { ReactNode } from 'react'; +import { FieldError, UseFormRegister, Path, FieldValues } from 'react-hook-form'; +import cn from 'clsx'; +import s from './FormSelectField.module.scss'; + +export type SelectOptionProps = { + text: string; + value: string; +}; + +const SelectOption = ({ text, value }: SelectOptionProps) => ; + +type FormSelectFieldProps = { + children: ReactNode; + register: UseFormRegister; + options: SelectOptionProps[]; + name: Path; + errors?: FieldError; +}; + +export const FormSelectField = ({ + children, + register, + options, + name, + errors, +}: FormSelectFieldProps) => ( +
+ + + {errors && {errors.message}} +
+); diff --git a/src/shared/forms/FormTextareaField/FormTextareaField.module.scss b/src/shared/forms/FormTextareaField/FormTextareaField.module.scss new file mode 100644 index 000000000..8568d3e8b --- /dev/null +++ b/src/shared/forms/FormTextareaField/FormTextareaField.module.scss @@ -0,0 +1,27 @@ +.container { + display: flex; + flex-direction: column; +} + +.label { + display: block; + margin-bottom: 0.5rem; + font-weight: bold; +} + +.textarea { + padding: 0.5rem; + border: 1px solid #ccc; + border-radius: 4px; + resize: vertical; + + &.error { + border-color: red; + } +} + +.error-message { + color: red; + font-size: 0.875rem; + padding-top: 0.3rem; +} diff --git a/src/shared/forms/FormTextareaField/FormTextareaField.tsx b/src/shared/forms/FormTextareaField/FormTextareaField.tsx new file mode 100644 index 000000000..b899037e6 --- /dev/null +++ b/src/shared/forms/FormTextareaField/FormTextareaField.tsx @@ -0,0 +1,26 @@ +import React, { ReactNode } from 'react'; +import { FieldError, UseFormRegister, Path, FieldValues } from 'react-hook-form'; +import cn from 'clsx'; +import s from './FormTextareaField.module.scss'; + +type FormTextareaFieldProps = { + children: ReactNode; + register: UseFormRegister; + name: Path; + rows?: number; + errors?: FieldError; +}; + +export const FormTextareaField = ({ + children, + rows, + register, + name, + errors, +}: FormTextareaFieldProps) => ( +
+ + + {errors && {errors.message}} +
+); diff --git a/src/shared/forms/RegularForm/RegularForm.module.scss b/src/shared/forms/RegularForm/RegularForm.module.scss new file mode 100644 index 000000000..aa7703b13 --- /dev/null +++ b/src/shared/forms/RegularForm/RegularForm.module.scss @@ -0,0 +1,14 @@ +.form { + max-width: 400px; + padding: 1rem; + border: 1px solid #ccc; + border-radius: 8px; + background-color: #f9f9f9; + display: flex; + flex-direction: column; + gap: 1rem; +} + +.title { + text-align: center; +} diff --git a/src/shared/forms/RegularForm/RegularForm.tsx b/src/shared/forms/RegularForm/RegularForm.tsx new file mode 100644 index 000000000..8e4f828e7 --- /dev/null +++ b/src/shared/forms/RegularForm/RegularForm.tsx @@ -0,0 +1,17 @@ +import React, { ReactNode, FormEventHandler } from 'react'; +import s from './RegularForm.module.scss'; + +type RegularFormProps = { + onSubmit: FormEventHandler; + children: ReactNode; + title?: string; +}; + +export const RegularForm = ({ onSubmit, title, children }: RegularFormProps) => { + return ( +
+ {title &&

{title}

} + {children} +
+ ); +}; diff --git a/src/stories/OperationForm.stories.ts b/src/stories/OperationForm.stories.ts new file mode 100644 index 000000000..87b7f0317 --- /dev/null +++ b/src/stories/OperationForm.stories.ts @@ -0,0 +1,13 @@ +import type { Meta } from '@storybook/react'; + +import { OperationForm } from '../pages/OperationForm/OperationForm'; + +const meta: Meta = { + component: OperationForm, + title: 'Формы/Операция', + tags: ['autodocs'], +}; + +export default meta; + +export const Test = {}; diff --git a/src/stories/ProductForm.stories.ts b/src/stories/ProductForm.stories.ts new file mode 100644 index 000000000..bdcef7588 --- /dev/null +++ b/src/stories/ProductForm.stories.ts @@ -0,0 +1,13 @@ +import type { Meta } from '@storybook/react'; + +import { ProductForm } from '../pages/ProductForm/ProductForm'; + +const meta: Meta = { + component: ProductForm, + title: 'Формы/Товар', + tags: ['autodocs'], +}; + +export default meta; + +export const Test = {}; diff --git a/src/stories/ProfileForm.stories.ts b/src/stories/ProfileForm.stories.ts new file mode 100644 index 000000000..98aa6cc44 --- /dev/null +++ b/src/stories/ProfileForm.stories.ts @@ -0,0 +1,13 @@ +import type { Meta } from '@storybook/react'; + +import { ProfileForm } from '../pages/ProfileForm/ProfileForm'; + +const meta: Meta = { + component: ProfileForm, + title: 'Формы/Профиль', + tags: ['autodocs'], +}; + +export default meta; + +export const Test = {}; diff --git a/src/stories/SignInForm.stories.ts b/src/stories/SignInForm.stories.ts new file mode 100644 index 000000000..57d9d6528 --- /dev/null +++ b/src/stories/SignInForm.stories.ts @@ -0,0 +1,13 @@ +import type { Meta } from '@storybook/react'; + +import { SignInForm } from '../pages/SignInForm/SignInForm'; + +const meta: Meta = { + component: SignInForm, + title: 'Формы/Вход', + tags: ['autodocs'], +}; + +export default meta; + +export const Test = {}; diff --git a/src/stories/SignUpForm.stories.ts b/src/stories/SignUpForm.stories.ts new file mode 100644 index 000000000..bb462cbec --- /dev/null +++ b/src/stories/SignUpForm.stories.ts @@ -0,0 +1,13 @@ +import type { Meta } from '@storybook/react'; + +import { SignUpForm } from '../pages/SignUpForm/SignUpForm'; + +const meta: Meta = { + component: SignUpForm, + title: 'Формы/Регистрация', + tags: ['autodocs'], +}; + +export default meta; + +export const Test = {}; From 053b3c87757874811993dfe45266f96b3d82e7b8 Mon Sep 17 00:00:00 2001 From: Igor Aralov Date: Mon, 27 Jan 2025 12:27:16 +0300 Subject: [PATCH 25/27] =?UTF-8?q?[10]=20=D0=A0=D0=B5=D0=B0=D0=BB=D0=B8?= =?UTF-8?q?=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=20AccountService=20=D1=87=D0=B5?= =?UTF-8?q?=D1=80=D0=B5=D0=B7=20=D0=BF=D0=BE=D0=B4=D1=85=D0=BE=D0=B4=20Tes?= =?UTF-8?q?t=20Driven=20Development=20=D0=BF=D0=BE=D0=B4=D1=85=D0=BE=D0=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../homework-10/AccountService.test.ts | 126 ++++++++++++++++++ src/homeworks/homework-10/AccountService.ts | 28 ++++ src/homeworks/homework-10/Database.ts | 33 +++++ src/homeworks/homework-10/DiscountService.ts | 117 ++++++++++++++++ src/homeworks/homework-10/Entities.ts | 13 ++ 5 files changed, 317 insertions(+) create mode 100644 src/homeworks/homework-10/AccountService.test.ts create mode 100644 src/homeworks/homework-10/AccountService.ts create mode 100644 src/homeworks/homework-10/Database.ts create mode 100644 src/homeworks/homework-10/DiscountService.ts create mode 100644 src/homeworks/homework-10/Entities.ts diff --git a/src/homeworks/homework-10/AccountService.test.ts b/src/homeworks/homework-10/AccountService.test.ts new file mode 100644 index 000000000..9ed718dc0 --- /dev/null +++ b/src/homeworks/homework-10/AccountService.test.ts @@ -0,0 +1,126 @@ +import { IDatabase } from './Database'; +import { IDiscountService, DiscountService } from './DiscountService'; +import { AccountService } from './AccountService'; +import { Product, User } from './Entities'; + +class MockDatabase implements IDatabase { + private data: Map = new Map(); + + async save(key: string, value: T): Promise { + this.data.set(key, value); + return true; + } + + async load(key: string): Promise { + const value = this.data.get(key); + if (value === undefined) { + throw new Error('Table not found'); + } + return { ...(value as T) }; + } +} + +describe('AccountService', () => { + let discountService: IDiscountService; + let accountService: AccountService; + + beforeEach(() => { + const db = new MockDatabase(); + discountService = new DiscountService(db); + accountService = new AccountService(discountService); + }); + + test('should throw table not found error', async () => { + try { + await accountService.userDiscount.loadDiscounts(); + } catch (e) { + expect(e.message).toBe('Failed to load user_discount discounts'); + } + }); + + test('should throw table not found error', async () => { + try { + await accountService.userDiscount.loadDiscounts(); + } catch (e) { + expect(e.message).toBe('Failed to load user_discount discounts'); + } + }); + + test('should set and get user discount', async () => { + await accountService.userDiscount.setUserDiscount(User.Standard, 10); + await accountService.userDiscount.saveDiscounts(); + await accountService.userDiscount.loadDiscounts(); + + expect(accountService.userDiscount.getUserDiscount(User.Standard)).toBe(10); + }); + + test('should get user discount = 0 for negative value', async () => { + await accountService.userDiscount.setUserDiscount(User.Standard, -10); + await accountService.userDiscount.saveDiscounts(); + await accountService.userDiscount.loadDiscounts(); + + expect(accountService.userDiscount.getUserDiscount(User.Standard)).toBe(0); + }); + + test('should calculate user discount = 100 for value > 100', async () => { + await accountService.userDiscount.setUserDiscount(User.Standard, 200); + await accountService.userDiscount.saveDiscounts(); + await accountService.userDiscount.loadDiscounts(); + + expect(accountService.userDiscount.getUserDiscount(User.Standard)).toBe(100); + }); + + test('should set and get user product discount', async () => { + await accountService.userProductDiscount.setUserProductDiscount(User.Standard, Product.Car, 10); + await accountService.userProductDiscount.saveDiscounts(); + await accountService.userProductDiscount.loadDiscounts(); + + expect(accountService.userProductDiscount.getUserProductDiscount(User.Standard, Product.Car)).toBe(10); + }); + + test('should get user product discount = 0 for negative value', async () => { + await accountService.userProductDiscount.setUserProductDiscount(User.Standard, Product.Car, -10); + await accountService.userProductDiscount.saveDiscounts(); + await accountService.userProductDiscount.loadDiscounts(); + + expect(accountService.userProductDiscount.getUserProductDiscount(User.Standard, Product.Car)).toBe(0); + }); + + test('should calculate user product discount = 100 for value > 100', async () => { + await accountService.userProductDiscount.setUserProductDiscount(User.Standard, Product.Car, 200); + await accountService.userProductDiscount.saveDiscounts(); + await accountService.userProductDiscount.loadDiscounts(); + + expect(accountService.userProductDiscount.getUserProductDiscount(User.Standard, Product.Car)).toBe(100); + }); + + test('should calculate total discount', async () => { + await accountService.userDiscount.setUserDiscount(User.Standard, 10); + await accountService.userProductDiscount.setUserProductDiscount(User.Standard, Product.Car, 20); + await accountService.saveAllDiscounts(); + await accountService.loadAllDiscounts(); + + const totalDiscount = accountService.calculateTotalDiscount(User.Standard, Product.Car); + expect(totalDiscount).toBe(30); + }); + + test('should calculate total discount = 0 for negative value', async () => { + await accountService.userDiscount.setUserDiscount(User.Standard, -10); + await accountService.userProductDiscount.setUserProductDiscount(User.Standard, Product.Car, -20); + await accountService.saveAllDiscounts(); + await accountService.loadAllDiscounts(); + + const totalDiscount = accountService.calculateTotalDiscount(User.Standard, Product.Car); + expect(totalDiscount).toBe(0); + }); + + test('should calculate total discount = 100 for value > 100', async () => { + await accountService.userDiscount.setUserDiscount(User.Standard, 200); + await accountService.userProductDiscount.setUserProductDiscount(User.Standard, Product.Car, 10); + await accountService.saveAllDiscounts(); + await accountService.loadAllDiscounts(); + + const totalDiscount = accountService.calculateTotalDiscount(User.Standard, Product.Car); + expect(totalDiscount).toBe(100); + }); +}); diff --git a/src/homeworks/homework-10/AccountService.ts b/src/homeworks/homework-10/AccountService.ts new file mode 100644 index 000000000..5cda78632 --- /dev/null +++ b/src/homeworks/homework-10/AccountService.ts @@ -0,0 +1,28 @@ +import { UserDiscount, UserProductDiscount, IDiscountService } from './DiscountService'; +import { Product, User } from './Entities'; + +export class AccountService { + userDiscount: UserDiscount; + userProductDiscount: UserProductDiscount; + private discountService: IDiscountService; + + constructor(discountService: IDiscountService) { + this.discountService = discountService; + this.userDiscount = new UserDiscount(this.discountService); + this.userProductDiscount = new UserProductDiscount(this.discountService); + } + + async loadAllDiscounts() { + Promise.all([this.userDiscount.loadDiscounts(), this.userProductDiscount.loadDiscounts()]); + } + + async saveAllDiscounts() { + Promise.all([this.userDiscount.saveDiscounts(), this.userProductDiscount.saveDiscounts()]); + } + + calculateTotalDiscount(user: User, product: Product) { + const userDiscount = this.userDiscount.getUserDiscount(user); + const userProductDiscount = this.userProductDiscount.getUserProductDiscount(user, product); + return this.discountService.totalDiscount(userDiscount, userProductDiscount); + } +} diff --git a/src/homeworks/homework-10/Database.ts b/src/homeworks/homework-10/Database.ts new file mode 100644 index 000000000..d053ce4b9 --- /dev/null +++ b/src/homeworks/homework-10/Database.ts @@ -0,0 +1,33 @@ +export interface IDatabase { + save(key: string, value: T): Promise; + load(key: string): Promise; +} + +export class Database implements IDatabase { + private data: Map; + + constructor() { + this.data = new Map(); + } + + save(key: string, value: T): Promise { + return new Promise((resolve) => + setTimeout(() => { + resolve(!!this.data.set(key, value)); + }, 500) + ); + } + + load(key: string): Promise { + return new Promise((resolve, reject) => + setTimeout(() => { + const value = this.data.get(key); + if (value === undefined) { + reject(new Error('Table not found')); + } else { + resolve({ ...(value as T) }); + } + }, 500) + ); + } +} \ No newline at end of file diff --git a/src/homeworks/homework-10/DiscountService.ts b/src/homeworks/homework-10/DiscountService.ts new file mode 100644 index 000000000..1a6c9fce9 --- /dev/null +++ b/src/homeworks/homework-10/DiscountService.ts @@ -0,0 +1,117 @@ +import { IDatabase } from './Database'; +import { User, Product } from './Entities'; + +export interface IDiscountService { + loadDiscounts(table: string): Promise; + saveDiscounts(table: string): Promise; + getDiscount(table: string, field: string): number; + setDiscount(table: string, field: string, value: number): void; + totalDiscount(...args: number[]): number; +} + +type Discount = Record; + +export class DiscountService implements IDiscountService { + private database: IDatabase; + private discounts: Record = {}; + private maxDiscountValue = 100; + private minDiscountValue = 0; + + constructor(db: IDatabase) { + this.database = db; + } + + private getValidDiscount(value: number) { + return value < this.minDiscountValue + ? this.minDiscountValue + : value > this.maxDiscountValue + ? this.maxDiscountValue + : value; + } + + async loadDiscounts(table: string) { + try { + this.discounts[table] = await this.database.load(table); + } catch (error) { + this.discounts[table] = {}; + throw new Error(`Failed to load ${table} discounts`); + } + } + + async saveDiscounts(table: string) { + try { + await this.database.save(table, this.discounts[table]); + } catch (error) { + console.error(`Failed to save ${table} discounts:`, error); + } + } + + getDiscount(table: string, field: string) { + return this.discounts[table]?.[field] ?? 0; + } + + setDiscount(table: string, field: string, value: number) { + if (!this.discounts[table]) { + this.discounts[table] = {}; + } + this.discounts[table][field] = this.getValidDiscount(value); + } + + totalDiscount(...args: number[]) { + return this.getValidDiscount(args.reduce((x, y) => x + y, 0)); + } +} + +abstract class BaseDiscount { + protected discountService: IDiscountService; + protected discountTable: string; + + constructor(discountService: IDiscountService, discountTable: string) { + this.discountService = discountService; + this.discountTable = discountTable; + } + + async loadDiscounts() { + await this.discountService.loadDiscounts(this.discountTable); + } + + async saveDiscounts() { + await this.discountService.saveDiscounts(this.discountTable); + } + + getDiscount(field: string) { + return this.discountService.getDiscount(this.discountTable, field); + } + + setDiscount(field: string, value: number) { + this.discountService.setDiscount(this.discountTable, field, value); + } +} + +export class UserDiscount extends BaseDiscount { + constructor(discountService: IDiscountService) { + super(discountService, 'user_discount'); + } + + getUserDiscount(user: User) { + return this.getDiscount(user); + } + + setUserDiscount(user: User, value: number) { + this.setDiscount(user, value); + } +} + +export class UserProductDiscount extends BaseDiscount { + constructor(discountService: IDiscountService) { + super(discountService, 'user_product_discount'); + } + + getUserProductDiscount(user: User, product: Product) { + return this.getDiscount(`${user}_${product}`); + } + + setUserProductDiscount(user: User, product: Product, value: number) { + this.setDiscount(`${user}_${product}`, value); + } +} diff --git a/src/homeworks/homework-10/Entities.ts b/src/homeworks/homework-10/Entities.ts new file mode 100644 index 000000000..2c06b867a --- /dev/null +++ b/src/homeworks/homework-10/Entities.ts @@ -0,0 +1,13 @@ +export enum User { + Standard = 'Standard', + Premium = 'Premium', + Gold = 'Gold', + Free = 'Free', + } + + export enum Product { + Car = 'Car', + Toy = 'Toy', + Food = 'Food', + } + \ No newline at end of file From bef1ea59b1612d7346774fbfa700243146af1eea Mon Sep 17 00:00:00 2001 From: Igor Aralov Date: Mon, 24 Feb 2025 22:36:02 +0300 Subject: [PATCH 26/27] =?UTF-8?q?[=D0=9F=D0=B0=D1=82=D1=82=D0=B5=D1=80?= =?UTF-8?q?=D0=BD=D1=8B]=20=D0=9E=D1=82=D1=80=D0=B5=D1=84=D0=B0=D0=BA?= =?UTF-8?q?=D1=82=D0=BE=D1=80=D0=B8=D0=BB=20=D0=BA=D0=BE=D0=BC=D0=BF=D0=BE?= =?UTF-8?q?=D0=BD=D0=B5=D0=BD=D1=82=D1=8B=20=D0=B8=D1=81=D0=BF=D0=BE=D0=BB?= =?UTF-8?q?=D1=8C=D0=B7=D1=83=D1=8F=20=D1=80=D0=B5=D0=B0=D0=BA=D1=82=20?= =?UTF-8?q?=D0=BF=D0=B0=D1=82=D1=82=D0=B5=D1=80=D0=BD=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/OperationForm/OperationForm.tsx | 3 +- src/pages/ProductForm/ProductForm.tsx | 3 +- src/pages/ProfileForm/ProfileForm.tsx | 6 +- src/pages/SignInForm/SignInForm.tsx | 3 +- src/pages/SignUpForm/SignUpForm.tsx | 3 +- src/shared/collapse/Collapse.tsx | 26 ++--- .../collapse/hooks/useCollapseHeight.ts | 26 +++++ src/shared/forms/RegularForm/RegularForm.tsx | 10 +- src/shared/tooltip-buttons/TooltipButtons.tsx | 3 +- src/shared/tooltip/Tooltip.tsx | 95 ++----------------- src/shared/tooltip/hooks/useTooltip.ts | 59 ++++++++++++ src/shared/tooltip/utils/tooltipPosition.ts | 42 ++++++++ 12 files changed, 161 insertions(+), 118 deletions(-) create mode 100644 src/shared/collapse/hooks/useCollapseHeight.ts create mode 100644 src/shared/tooltip/hooks/useTooltip.ts create mode 100644 src/shared/tooltip/utils/tooltipPosition.ts diff --git a/src/pages/OperationForm/OperationForm.tsx b/src/pages/OperationForm/OperationForm.tsx index a3153be53..e7aa2902c 100644 --- a/src/pages/OperationForm/OperationForm.tsx +++ b/src/pages/OperationForm/OperationForm.tsx @@ -34,7 +34,8 @@ export const OperationForm = () => { }; return ( - + + Операция { }; return ( - + + Товар Название diff --git a/src/pages/ProfileForm/ProfileForm.tsx b/src/pages/ProfileForm/ProfileForm.tsx index 4ae335d8e..e9c5b7972 100644 --- a/src/pages/ProfileForm/ProfileForm.tsx +++ b/src/pages/ProfileForm/ProfileForm.tsx @@ -30,7 +30,8 @@ const ChangeProfileForm = () => { }; return ( - + + Изменить профиль Псевдоним @@ -57,7 +58,8 @@ const ChangePasswordForm = () => { }; return ( - + + Изменить пароль Пароль diff --git a/src/pages/SignInForm/SignInForm.tsx b/src/pages/SignInForm/SignInForm.tsx index 08b60f2de..bb614e6b4 100644 --- a/src/pages/SignInForm/SignInForm.tsx +++ b/src/pages/SignInForm/SignInForm.tsx @@ -25,7 +25,8 @@ export const SignInForm = () => { }; return ( - + + Войти Email diff --git a/src/pages/SignUpForm/SignUpForm.tsx b/src/pages/SignUpForm/SignUpForm.tsx index d714f03eb..64676cf7f 100644 --- a/src/pages/SignUpForm/SignUpForm.tsx +++ b/src/pages/SignUpForm/SignUpForm.tsx @@ -25,7 +25,8 @@ export const SignUpForm = () => { }; return ( - + + Зарегистрироваться Email diff --git a/src/shared/collapse/Collapse.tsx b/src/shared/collapse/Collapse.tsx index a5b8a730f..d61e5934c 100644 --- a/src/shared/collapse/Collapse.tsx +++ b/src/shared/collapse/Collapse.tsx @@ -1,4 +1,5 @@ -import React, { useState, useRef, useEffect, ReactNode } from 'react'; +import React, { useState, ReactNode } from 'react'; +import { useCollapseHeight } from './hooks/useCollapseHeight'; import s from './Collapse.module.scss'; type CollapseProps = { @@ -6,29 +7,14 @@ type CollapseProps = { children: ReactNode; }; -export const Collapse = ({ title, children }: CollapseProps) => { +export const Collapse: React.FC = ({ title, children }) => { const [isOpen, setIsOpen] = useState(false); - const [height, setHeight] = useState(0); - const contentRef = useRef(null); + const { height, contentRef } = useCollapseHeight(isOpen); const toggleCollapse = () => { - setIsOpen(!isOpen); + setIsOpen((prev) => !prev); }; - useEffect(() => { - const resizeObserver = new ResizeObserver((entries) => - entries.forEach((entry) => setHeight(entry.borderBoxSize[0].blockSize)) - ); - - const currentContentRef = contentRef.current; - - currentContentRef && resizeObserver.observe(currentContentRef); - - return () => { - currentContentRef && resizeObserver.unobserve(currentContentRef); - }; - }, []); - return (
{ + const [height, setHeight] = useState(0); + const contentRef = useRef(null); + + useEffect(() => { + const resizeObserver = new ResizeObserver((entries) => { + entries.forEach((entry) => setHeight(entry.borderBoxSize[0].blockSize)); + }); + + const currentContentRef = contentRef.current; + + if (currentContentRef) { + resizeObserver.observe(currentContentRef); + } + + return () => { + if (currentContentRef) { + resizeObserver.unobserve(currentContentRef); + } + }; + }, []); + + return { height, contentRef }; + }; \ No newline at end of file diff --git a/src/shared/forms/RegularForm/RegularForm.tsx b/src/shared/forms/RegularForm/RegularForm.tsx index 8e4f828e7..586a45019 100644 --- a/src/shared/forms/RegularForm/RegularForm.tsx +++ b/src/shared/forms/RegularForm/RegularForm.tsx @@ -4,14 +4,18 @@ import s from './RegularForm.module.scss'; type RegularFormProps = { onSubmit: FormEventHandler; children: ReactNode; - title?: string; }; -export const RegularForm = ({ onSubmit, title, children }: RegularFormProps) => { +const RegularForm = ({ onSubmit, children }: RegularFormProps) => { return (
- {title &&

{title}

} {children}
); }; + +const Title = ({ children }: { children: ReactNode }) =>

{children}

; + +RegularForm.Title = Title; + +export { RegularForm }; diff --git a/src/shared/tooltip-buttons/TooltipButtons.tsx b/src/shared/tooltip-buttons/TooltipButtons.tsx index 37d399c0d..26e3deaa4 100644 --- a/src/shared/tooltip-buttons/TooltipButtons.tsx +++ b/src/shared/tooltip-buttons/TooltipButtons.tsx @@ -1,5 +1,6 @@ import React, { ReactNode } from 'react'; -import { Position, Tooltip } from '../tooltip/Tooltip'; +import { Position } from '../tooltip/utils/tooltipPosition'; +import {Tooltip} from "../tooltip/Tooltip"; import { Button } from '../button/Button'; import s from './TooltipButtons.module.scss'; diff --git a/src/shared/tooltip/Tooltip.tsx b/src/shared/tooltip/Tooltip.tsx index d91366f9e..9b9164918 100644 --- a/src/shared/tooltip/Tooltip.tsx +++ b/src/shared/tooltip/Tooltip.tsx @@ -1,51 +1,10 @@ -import React, { useState, useRef, useLayoutEffect, ReactNode } from 'react'; +import React, { ReactNode } from 'react'; import { createPortal } from 'react-dom'; +import { useTooltip } from './hooks/useTooltip'; +import { Position } from './utils/tooltipPosition'; import cn from 'clsx'; import s from './Tooltip.module.scss'; -type Coords = { - top: number; - left: number; -}; - -type CoordProps = { - targetRect: DOMRect; - tooltipRect: DOMRect; - offset: number; -}; - -export type Position = 'top' | 'bottom' | 'left' | 'right'; - -type PositionMap = Record Coords>; - -const getCenterCoord = (primary: number, secondary: number) => (primary - secondary) / 2; - -const YLeft = (primary: DOMRect, secondary: DOMRect) => - primary.left + window.scrollX + getCenterCoord(primary.width, secondary.width); -const XTop = (primary: DOMRect, secondary: DOMRect) => - primary.top + window.scrollY + getCenterCoord(primary.height, secondary.height); - -const positionMap: PositionMap = { - top: ({ targetRect, tooltipRect, offset }) => ({ - top: targetRect.top + window.scrollY - tooltipRect.height - offset, - left: targetRect.left + window.scrollX + getCenterCoord(targetRect.width, tooltipRect.width), - }), - - bottom: ({ targetRect, tooltipRect, offset }) => ({ - top: targetRect.bottom + window.scrollY + offset, - left: YLeft(targetRect, tooltipRect), - }), - left: ({ targetRect, tooltipRect, offset }) => ({ - top: XTop(targetRect, tooltipRect), - left: targetRect.left + window.scrollX - tooltipRect.width - offset, - }), - - right: ({ targetRect, tooltipRect, offset }) => ({ - top: XTop(targetRect, tooltipRect), - left: targetRect.left + window.scrollX + targetRect.width + offset, - }), -}; - type TooltipProps = { children: ReactNode; content: ReactNode; @@ -54,50 +13,10 @@ type TooltipProps = { }; export const Tooltip = ({ children, content, duration = 1000, position = 'bottom' }: TooltipProps) => { - const [visible, setVisible] = useState(false); - const [mounted, setMounted] = useState(false); - const [coords, setCoords] = useState({ top: 0, left: 0 }); - const tooltipRef = useRef(null); - const targetRef = useRef(null); - const timerRef = useRef(null); - const mountTimerRef = useRef(null); - - const mountTimer = 50; - - const clearTimeouts = () => { - timerRef.current && clearTimeout(timerRef.current); - mountTimerRef.current && clearTimeout(mountTimerRef.current); - }; - - const handleMouseEnter = () => { - clearTimeouts(); - setMounted(true); - mountTimerRef.current = setTimeout(() => setVisible(true), mountTimer); - }; - - const handleMouseLeave = () => { - setVisible(false); - timerRef.current = setTimeout(() => setMounted(false), duration + mountTimer); - }; - - useLayoutEffect(() => { - const target = targetRef.current; - const tooltip = tooltipRef.current; - - if (!target || !tooltip) return; - - if (mounted) { - tooltipRef.current?.style.setProperty('--tooltip-animation-ms', `${duration + mountTimer}ms`); - const targetRect = target.getBoundingClientRect(); - const tooltipRect = tooltip.getBoundingClientRect(); - const calcPosition = positionMap[position] ?? positionMap['bottom']; - setCoords(calcPosition({ targetRect, tooltipRect, offset: 5 })); - } - - return () => { - clearTimeouts(); - }; - }, [mounted]); + const { visible, mounted, coords, tooltipRef, targetRef, handleMouseEnter, handleMouseLeave } = useTooltip( + position, + duration + ); return ( <> diff --git a/src/shared/tooltip/hooks/useTooltip.ts b/src/shared/tooltip/hooks/useTooltip.ts new file mode 100644 index 000000000..0304c6fd8 --- /dev/null +++ b/src/shared/tooltip/hooks/useTooltip.ts @@ -0,0 +1,59 @@ +import { useState, useRef, useLayoutEffect } from 'react'; +import { positionMap, Position } from '../utils/tooltipPosition'; + +export const useTooltip = (position: Position = 'bottom', duration: number = 1000) => { + const [visible, setVisible] = useState(false); + const [mounted, setMounted] = useState(false); + const [coords, setCoords] = useState({ top: 0, left: 0 }); + const tooltipRef = useRef(null); + const targetRef = useRef(null); + const timerRef = useRef(null); + const mountTimerRef = useRef(null); + + const mountTimer = 50; + + const clearTimeouts = () => { + if (timerRef.current) clearTimeout(timerRef.current); + if (mountTimerRef.current) clearTimeout(mountTimerRef.current); + }; + + const handleMouseEnter = () => { + clearTimeouts(); + setMounted(true); + mountTimerRef.current = window.setTimeout(() => setVisible(true), mountTimer); + }; + + const handleMouseLeave = () => { + setVisible(false); + timerRef.current = window.setTimeout(() => setMounted(false), duration + mountTimer); + }; + + useLayoutEffect(() => { + const target = targetRef.current; + const tooltip = tooltipRef.current; + + if (!target || !tooltip) return; + + if (mounted) { + tooltipRef.current?.style.setProperty('--tooltip-animation-ms', `${duration + mountTimer}ms`); + const targetRect = target.getBoundingClientRect(); + const tooltipRect = tooltip.getBoundingClientRect(); + const calcPosition = positionMap[position] ?? positionMap['bottom']; + setCoords(calcPosition({ targetRect, tooltipRect, offset: 5 })); + } + + return () => { + clearTimeouts(); + }; + }, [mounted, position]); + + return { + visible, + mounted, + coords, + tooltipRef, + targetRef, + handleMouseEnter, + handleMouseLeave, + }; +}; \ No newline at end of file diff --git a/src/shared/tooltip/utils/tooltipPosition.ts b/src/shared/tooltip/utils/tooltipPosition.ts new file mode 100644 index 000000000..c71520b60 --- /dev/null +++ b/src/shared/tooltip/utils/tooltipPosition.ts @@ -0,0 +1,42 @@ +type Coords = { + top: number; + left: number; + }; + + type CoordProps = { + targetRect: DOMRect; + tooltipRect: DOMRect; + offset: number; + }; + + export type Position = 'top' | 'bottom' | 'left' | 'right'; + + const getCenterCoord = (primary: number, secondary: number) => (primary - secondary) / 2; + + const YLeft = (primary: DOMRect, secondary: DOMRect) => + primary.left + window.scrollX + getCenterCoord(primary.width, secondary.width); + + const XTop = (primary: DOMRect, secondary: DOMRect) => + primary.top + window.scrollY + getCenterCoord(primary.height, secondary.height); + + export const positionMap: Record Coords> = { + top: ({ targetRect, tooltipRect, offset }) => ({ + top: targetRect.top + window.scrollY - tooltipRect.height - offset, + left: targetRect.left + window.scrollX + getCenterCoord(targetRect.width, tooltipRect.width), + }), + + bottom: ({ targetRect, tooltipRect, offset }) => ({ + top: targetRect.bottom + window.scrollY + offset, + left: YLeft(targetRect, tooltipRect), + }), + + left: ({ targetRect, tooltipRect, offset }) => ({ + top: XTop(targetRect, tooltipRect), + left: targetRect.left + window.scrollX - tooltipRect.width - offset, + }), + + right: ({ targetRect, tooltipRect, offset }) => ({ + top: XTop(targetRect, tooltipRect), + left: targetRect.left + window.scrollX + targetRect.width + offset, + }), + }; \ No newline at end of file From cfce58a00bf469fc7792edd52ff5a78c96753870 Mon Sep 17 00:00:00 2001 From: Igor Aralov Date: Mon, 24 Feb 2025 22:48:31 +0300 Subject: [PATCH 27/27] Fix linter errors --- .../homework-10/AccountService.test.ts | 13 +-- src/homeworks/homework-10/Database.ts | 4 +- src/homeworks/homework-10/Entities.ts | 23 +++--- src/pages/ProductForm/ProductForm.tsx | 2 +- src/shared/collapse/Collapse.tsx | 2 +- .../collapse/hooks/useCollapseHeight.ts | 46 +++++------ src/shared/tooltip-buttons/TooltipButtons.tsx | 2 +- src/shared/tooltip/hooks/useTooltip.ts | 4 +- src/shared/tooltip/utils/tooltipPosition.ts | 82 +++++++++---------- 9 files changed, 86 insertions(+), 92 deletions(-) diff --git a/src/homeworks/homework-10/AccountService.test.ts b/src/homeworks/homework-10/AccountService.test.ts index 9ed718dc0..b2cc09884 100644 --- a/src/homeworks/homework-10/AccountService.test.ts +++ b/src/homeworks/homework-10/AccountService.test.ts @@ -31,18 +31,13 @@ describe('AccountService', () => { }); test('should throw table not found error', async () => { + let err; try { await accountService.userDiscount.loadDiscounts(); } catch (e) { - expect(e.message).toBe('Failed to load user_discount discounts'); - } - }); - - test('should throw table not found error', async () => { - try { - await accountService.userDiscount.loadDiscounts(); - } catch (e) { - expect(e.message).toBe('Failed to load user_discount discounts'); + err = e; + } finally { + expect(err.message).toBe('Failed to load user_discount discounts'); } }); diff --git a/src/homeworks/homework-10/Database.ts b/src/homeworks/homework-10/Database.ts index d053ce4b9..4eee64319 100644 --- a/src/homeworks/homework-10/Database.ts +++ b/src/homeworks/homework-10/Database.ts @@ -13,7 +13,7 @@ export class Database implements IDatabase { save(key: string, value: T): Promise { return new Promise((resolve) => setTimeout(() => { - resolve(!!this.data.set(key, value)); + resolve(!!this.data.set(key, value)); }, 500) ); } @@ -30,4 +30,4 @@ export class Database implements IDatabase { }, 500) ); } -} \ No newline at end of file +} diff --git a/src/homeworks/homework-10/Entities.ts b/src/homeworks/homework-10/Entities.ts index 2c06b867a..c3d92df78 100644 --- a/src/homeworks/homework-10/Entities.ts +++ b/src/homeworks/homework-10/Entities.ts @@ -1,13 +1,12 @@ export enum User { - Standard = 'Standard', - Premium = 'Premium', - Gold = 'Gold', - Free = 'Free', - } - - export enum Product { - Car = 'Car', - Toy = 'Toy', - Food = 'Food', - } - \ No newline at end of file + Standard = 'Standard', + Premium = 'Premium', + Gold = 'Gold', + Free = 'Free', +} + +export enum Product { + Car = 'Car', + Toy = 'Toy', + Food = 'Food', +} diff --git a/src/pages/ProductForm/ProductForm.tsx b/src/pages/ProductForm/ProductForm.tsx index 00bf326fd..0fe98bc6b 100644 --- a/src/pages/ProductForm/ProductForm.tsx +++ b/src/pages/ProductForm/ProductForm.tsx @@ -24,7 +24,7 @@ export const ProductForm = () => { return ( - Товар + Товар Название diff --git a/src/shared/collapse/Collapse.tsx b/src/shared/collapse/Collapse.tsx index d61e5934c..da893a0b2 100644 --- a/src/shared/collapse/Collapse.tsx +++ b/src/shared/collapse/Collapse.tsx @@ -9,7 +9,7 @@ type CollapseProps = { export const Collapse: React.FC = ({ title, children }) => { const [isOpen, setIsOpen] = useState(false); - const { height, contentRef } = useCollapseHeight(isOpen); + const { height, contentRef } = useCollapseHeight(); const toggleCollapse = () => { setIsOpen((prev) => !prev); diff --git a/src/shared/collapse/hooks/useCollapseHeight.ts b/src/shared/collapse/hooks/useCollapseHeight.ts index 7a9eec406..4c4fb237c 100644 --- a/src/shared/collapse/hooks/useCollapseHeight.ts +++ b/src/shared/collapse/hooks/useCollapseHeight.ts @@ -1,26 +1,26 @@ -import React, { useEffect, useRef, useState } from "react"; +import React, { useEffect, useRef, useState } from 'react'; -export const useCollapseHeight = (isOpen: boolean) => { - const [height, setHeight] = useState(0); - const contentRef = useRef(null); - - useEffect(() => { - const resizeObserver = new ResizeObserver((entries) => { - entries.forEach((entry) => setHeight(entry.borderBoxSize[0].blockSize)); - }); - - const currentContentRef = contentRef.current; - +export const useCollapseHeight = () => { + const [height, setHeight] = useState(0); + const contentRef = useRef(null); + + useEffect(() => { + const resizeObserver = new ResizeObserver((entries) => { + entries.forEach((entry) => setHeight(entry.borderBoxSize[0].blockSize)); + }); + + const currentContentRef = contentRef.current; + + if (currentContentRef) { + resizeObserver.observe(currentContentRef); + } + + return () => { if (currentContentRef) { - resizeObserver.observe(currentContentRef); + resizeObserver.unobserve(currentContentRef); } - - return () => { - if (currentContentRef) { - resizeObserver.unobserve(currentContentRef); - } - }; - }, []); - - return { height, contentRef }; - }; \ No newline at end of file + }; + }, []); + + return { height, contentRef }; +}; diff --git a/src/shared/tooltip-buttons/TooltipButtons.tsx b/src/shared/tooltip-buttons/TooltipButtons.tsx index 26e3deaa4..87021d8fb 100644 --- a/src/shared/tooltip-buttons/TooltipButtons.tsx +++ b/src/shared/tooltip-buttons/TooltipButtons.tsx @@ -1,6 +1,6 @@ import React, { ReactNode } from 'react'; import { Position } from '../tooltip/utils/tooltipPosition'; -import {Tooltip} from "../tooltip/Tooltip"; +import { Tooltip } from '../tooltip/Tooltip'; import { Button } from '../button/Button'; import s from './TooltipButtons.module.scss'; diff --git a/src/shared/tooltip/hooks/useTooltip.ts b/src/shared/tooltip/hooks/useTooltip.ts index 0304c6fd8..f814037a7 100644 --- a/src/shared/tooltip/hooks/useTooltip.ts +++ b/src/shared/tooltip/hooks/useTooltip.ts @@ -1,7 +1,7 @@ import { useState, useRef, useLayoutEffect } from 'react'; import { positionMap, Position } from '../utils/tooltipPosition'; -export const useTooltip = (position: Position = 'bottom', duration: number = 1000) => { +export const useTooltip = (position: Position = 'bottom', duration = 1000) => { const [visible, setVisible] = useState(false); const [mounted, setMounted] = useState(false); const [coords, setCoords] = useState({ top: 0, left: 0 }); @@ -56,4 +56,4 @@ export const useTooltip = (position: Position = 'bottom', duration: number = 100 handleMouseEnter, handleMouseLeave, }; -}; \ No newline at end of file +}; diff --git a/src/shared/tooltip/utils/tooltipPosition.ts b/src/shared/tooltip/utils/tooltipPosition.ts index c71520b60..870e76073 100644 --- a/src/shared/tooltip/utils/tooltipPosition.ts +++ b/src/shared/tooltip/utils/tooltipPosition.ts @@ -1,42 +1,42 @@ type Coords = { - top: number; - left: number; - }; - - type CoordProps = { - targetRect: DOMRect; - tooltipRect: DOMRect; - offset: number; - }; - - export type Position = 'top' | 'bottom' | 'left' | 'right'; - - const getCenterCoord = (primary: number, secondary: number) => (primary - secondary) / 2; - - const YLeft = (primary: DOMRect, secondary: DOMRect) => - primary.left + window.scrollX + getCenterCoord(primary.width, secondary.width); - - const XTop = (primary: DOMRect, secondary: DOMRect) => - primary.top + window.scrollY + getCenterCoord(primary.height, secondary.height); - - export const positionMap: Record Coords> = { - top: ({ targetRect, tooltipRect, offset }) => ({ - top: targetRect.top + window.scrollY - tooltipRect.height - offset, - left: targetRect.left + window.scrollX + getCenterCoord(targetRect.width, tooltipRect.width), - }), - - bottom: ({ targetRect, tooltipRect, offset }) => ({ - top: targetRect.bottom + window.scrollY + offset, - left: YLeft(targetRect, tooltipRect), - }), - - left: ({ targetRect, tooltipRect, offset }) => ({ - top: XTop(targetRect, tooltipRect), - left: targetRect.left + window.scrollX - tooltipRect.width - offset, - }), - - right: ({ targetRect, tooltipRect, offset }) => ({ - top: XTop(targetRect, tooltipRect), - left: targetRect.left + window.scrollX + targetRect.width + offset, - }), - }; \ No newline at end of file + top: number; + left: number; +}; + +type CoordProps = { + targetRect: DOMRect; + tooltipRect: DOMRect; + offset: number; +}; + +export type Position = 'top' | 'bottom' | 'left' | 'right'; + +const getCenterCoord = (primary: number, secondary: number) => (primary - secondary) / 2; + +const YLeft = (primary: DOMRect, secondary: DOMRect) => + primary.left + window.scrollX + getCenterCoord(primary.width, secondary.width); + +const XTop = (primary: DOMRect, secondary: DOMRect) => + primary.top + window.scrollY + getCenterCoord(primary.height, secondary.height); + +export const positionMap: Record Coords> = { + top: ({ targetRect, tooltipRect, offset }) => ({ + top: targetRect.top + window.scrollY - tooltipRect.height - offset, + left: targetRect.left + window.scrollX + getCenterCoord(targetRect.width, tooltipRect.width), + }), + + bottom: ({ targetRect, tooltipRect, offset }) => ({ + top: targetRect.bottom + window.scrollY + offset, + left: YLeft(targetRect, tooltipRect), + }), + + left: ({ targetRect, tooltipRect, offset }) => ({ + top: XTop(targetRect, tooltipRect), + left: targetRect.left + window.scrollX - tooltipRect.width - offset, + }), + + right: ({ targetRect, tooltipRect, offset }) => ({ + top: XTop(targetRect, tooltipRect), + left: targetRect.left + window.scrollX + targetRect.width + offset, + }), +};