From 66c1323f0062bbd21b6662d25266c4239956ca22 Mon Sep 17 00:00:00 2001 From: RustamRu Date: Sun, 17 Nov 2024 18:05:24 +0300 Subject: [PATCH 01/13] feat: move i18n type utils to lib and describe (#33) --- src/lib/index.ts | 1 + src/lib/types.ts | 33 +++++++++++++++++++++++++++++++++ src/models/i18next.d.ts | 23 ++--------------------- 3 files changed, 36 insertions(+), 21 deletions(-) create mode 100644 src/lib/index.ts create mode 100644 src/lib/types.ts diff --git a/src/lib/index.ts b/src/lib/index.ts new file mode 100644 index 0000000..fcdac2d --- /dev/null +++ b/src/lib/index.ts @@ -0,0 +1 @@ +export * from './types'; \ No newline at end of file diff --git a/src/lib/types.ts b/src/lib/types.ts new file mode 100644 index 0000000..b244840 --- /dev/null +++ b/src/lib/types.ts @@ -0,0 +1,33 @@ +/** + * @example type Output = Split<'a.b1' | 'a.b2', '.'>; + * // ["a", "b1"] | ["a", "b2"] +*/ +type Split = + S extends `${infer A}${D}${infer B}` ? [A, ...Split] : [S]; + +/** + * @example type Output = NestedObject<["a", "b1"] | ["a", "b2"]>; + * // { a: { b1: string; }; } | { a: { b2: string; }; } +*/ +type NestedObject = + T extends [infer Head, ...infer Tail] ? + Head extends string ? + { [key in Head]: NestedObject } : never : + string; + +/** + * @example type Output = UnionToIntersection<{ a: { b1: string; }; } | { a: { b2: string; }; }>; + * // { a: { b1: string; }; } & { a: { b2: string; }; } +*/ +type UnionToIntersection = + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never; + +/** + * @example type Output = CreateTree<'a.b1' | 'a.b2', '.'>; + * // { a: { b1: string; }; } & { a: { b2: string; }; } +*/ +export type CreateTree = + UnionToIntersection> : never : never>; \ No newline at end of file diff --git a/src/models/i18next.d.ts b/src/models/i18next.d.ts index 7a1a687..7044fc2 100644 --- a/src/models/i18next.d.ts +++ b/src/models/i18next.d.ts @@ -1,24 +1,5 @@ import defaultLocale from '../../locales/ru.json'; - -type Split = - S extends `${infer A}${D}${infer B}` ? [A, ...Split] : [S]; - -type NestedObject = - T extends [infer Head, ...infer Tail] ? - Head extends string ? - { [key in Head]: NestedObject } : never : - string; - -// Основная утилита для обработки union type -type CreateTree = - UnionToIntersection> : never : never>; - -// Утилита для объединения типов -type UnionToIntersection = - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never; +import { CreateTree } from '../lib'; type LanguageResource = CreateTree; @@ -26,6 +7,6 @@ declare module "i18next" { interface CustomTypeOptions { resources: { '~': LanguageResource - }; + } } } \ No newline at end of file -- 2.45.2 From 0cd7ec8b226dd7c7a90d86bacd49365ac67c3d36 Mon Sep 17 00:00:00 2001 From: RustamRu Date: Sun, 17 Nov 2024 18:06:55 +0300 Subject: [PATCH 02/13] feat: create success stubs json with type generation (#33) --- package-lock.json | 102 ++++++++++++++++--- package.json | 1 + stubs/json/landing/landing-success.json | 27 +++++ stubs/json/landing/landing-success.json.d.ts | 28 +++++ 4 files changed, 144 insertions(+), 14 deletions(-) create mode 100644 stubs/json/landing/landing-success.json create mode 100644 stubs/json/landing/landing-success.json.d.ts diff --git a/package-lock.json b/package-lock.json index 93317ed..acfeaef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,6 +35,7 @@ "eslint-plugin-react": "^7.37.2", "globals": "^15.11.0", "prettier": "3.3.3", + "ts-json-as-const": "^1.0.7", "typescript-eslint": "^8.12.2" } }, @@ -2864,6 +2865,13 @@ "react": ">=18" } }, + "node_modules/@dfoverdx/tocamelcase": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@dfoverdx/tocamelcase/-/tocamelcase-1.0.7.tgz", + "integrity": "sha512-QDlMJqwcE4eVCvxxQXp8nh7Nw9m5VQHPCAiyTD+W86Tl89VGhVJRb//RJRZKpn5A/Bq3EQNYDYlepurQ805MOQ==", + "dev": true, + "license": "ISC" + }, "node_modules/@emotion/babel-plugin": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.12.0.tgz", @@ -6958,20 +6966,6 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "license": "ISC" }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -7043,6 +7037,13 @@ "node": ">=6" } }, + "node_modules/get-own-enumerable-property-symbols": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz", + "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==", + "dev": true, + "license": "ISC" + }, "node_modules/get-symbol-description": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", @@ -7839,6 +7840,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-3.0.0.tgz", + "integrity": "sha512-IlsXEHOjtKhpN8r/tRFj2nDyTmHvcfNeu/nrRIcXE17ROeatXchkojffa1SpdqW4cr/Fj6QkEf/Gn4zf6KKvEQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-path-cwd": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", @@ -7901,6 +7915,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-regexp": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-3.1.0.tgz", + "integrity": "sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-root": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-root/-/is-root-2.1.0.tgz", @@ -8049,6 +8076,19 @@ "dev": true, "license": "MIT" }, + "node_modules/isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -10404,6 +10444,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/stringify-object": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-4.0.1.tgz", + "integrity": "sha512-qpV1FBpN0R1gDAhCHIU71SYGZb35Te+gOQbQ6lYRmVJT7pF1NB8mkHeEJvyYNiHXw+fB4KIbeIjQl1rgiIijiA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "get-own-enumerable-property-symbols": "^3.0.2", + "is-obj": "^3.0.0", + "is-regexp": "^3.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -10704,6 +10759,25 @@ "typescript": ">=4.2.0" } }, + "node_modules/ts-json-as-const": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/ts-json-as-const/-/ts-json-as-const-1.0.7.tgz", + "integrity": "sha512-UMM24g4uBevqBuEqeMUNm2yBEd6VrpJt2hhGWSpmr3nGuhQYwYcLpmxUjsAh5qxJbafF+ICrHvOvHn16zh9Ojg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@dfoverdx/tocamelcase": "^1.0.7", + "isbinaryfile": "^4.0.8", + "json5": "^2.2.0", + "stringify-object": "^4.0.0" + }, + "bin": { + "ts-json-as-const": "index.js" + }, + "peerDependencies": { + "typescript": ">=3" + } + }, "node_modules/tsconfig-paths": { "version": "3.15.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", diff --git a/package.json b/package.json index 5b5ff92..652db04 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "eslint-plugin-react": "^7.37.2", "globals": "^15.11.0", "prettier": "3.3.3", + "ts-json-as-const": "^1.0.7", "typescript-eslint": "^8.12.2" } } diff --git a/stubs/json/landing/landing-success.json b/stubs/json/landing/landing-success.json new file mode 100644 index 0000000..d6ca8b6 --- /dev/null +++ b/stubs/json/landing/landing-success.json @@ -0,0 +1,27 @@ +{ + "success": true, + "body": { + "hero-section": { + "headline": "hero-section.headline", + "description": "hero-section.description", + "video": "demo.mp4" + }, + "sections": [ + { + "type": "benefits-section", + "heading": "benefits-section.heading", + "description": "benefits-section.description", + "list": [ + "benefits-section.list.0", + "benefits-section.list.1", + "benefits-section.list.2", + "benefits-section.list.3" + ] + }, + { + "type": "social-proof-section", + "heading": "social-proof-section.heading" + } + ] + } +} \ No newline at end of file diff --git a/stubs/json/landing/landing-success.json.d.ts b/stubs/json/landing/landing-success.json.d.ts new file mode 100644 index 0000000..7197ccf --- /dev/null +++ b/stubs/json/landing/landing-success.json.d.ts @@ -0,0 +1,28 @@ +interface LandingSuccess { + success: true, + body: { + 'hero-section': { + headline: 'hero-section.headline', + description: 'hero-section.description', + video: 'demo.mp4' + }, + sections: [ + { + type: 'benefits-section', + heading: 'benefits-section.heading', + description: 'benefits-section.description', + list: [ + 'benefits-section.list.0', + 'benefits-section.list.1', + 'benefits-section.list.2', + 'benefits-section.list.3' + ] + }, + {type: 'social-proof-section', heading: 'social-proof-section.heading'} + ] + } +} + +declare const LandingSuccess: LandingSuccess; + +export = LandingSuccess; \ No newline at end of file -- 2.45.2 From 5ca3fd2613f467f8ff84fc1cc13f123e842c2aa2 Mon Sep 17 00:00:00 2001 From: RustamRu Date: Sun, 17 Nov 2024 18:07:35 +0300 Subject: [PATCH 03/13] feat: apply success stubs to landing content (#33) --- .../BenefitsSection/BenefitsSection.tsx | 47 +++++-------------- .../landing/BenefitsSection/helper.ts | 11 +++++ .../landing/BenefitsSection/index.ts | 1 + .../landing/BenefitsSection/types.ts | 14 ++++++ .../landing/CtaButton/CtaButton.tsx | 4 +- .../landing/HeroSection/HeroSection.tsx | 21 ++++----- src/components/landing/HeroSection/types.ts | 9 ++++ .../SocialProofSection/SocialProofSection.tsx | 11 +++-- .../landing/SocialProofSection/index.ts | 1 + .../landing/SocialProofSection/types.ts | 5 ++ src/lib/types.ts | 8 ++++ src/pages/landing/index.tsx | 25 ++++++---- src/pages/landing/types.ts | 15 ++++++ 13 files changed, 109 insertions(+), 63 deletions(-) create mode 100644 src/components/landing/BenefitsSection/helper.ts create mode 100644 src/components/landing/BenefitsSection/types.ts create mode 100644 src/components/landing/HeroSection/types.ts create mode 100644 src/components/landing/SocialProofSection/types.ts create mode 100644 src/pages/landing/types.ts diff --git a/src/components/landing/BenefitsSection/BenefitsSection.tsx b/src/components/landing/BenefitsSection/BenefitsSection.tsx index dc74dbf..3270aef 100644 --- a/src/components/landing/BenefitsSection/BenefitsSection.tsx +++ b/src/components/landing/BenefitsSection/BenefitsSection.tsx @@ -1,52 +1,29 @@ import React, { FC } from 'react'; import { useTranslation } from 'react-i18next'; -import { - MdEco, - MdMiscellaneousServices, - MdPlace, - MdHandshake, -} from 'react-icons/md'; import { Heading, HStack, List, Text, VStack } from '@chakra-ui/react'; import { CtaButton, PageSection } from '../'; import { ListItem } from './ListItem'; +import { BenefitsSectionProps } from './types'; +import { iconsMap } from './helper'; -export const BenefitsSection: FC = () => { - const { t } = useTranslation('~', { - keyPrefix: 'dry-wash.landing.benefits-section', - }); - - const listData = [ - { - Icon: MdEco, - children: t('list.0'), - }, - { - Icon: MdMiscellaneousServices, - children: t('list.1'), - }, - { - Icon: MdPlace, - children: t('list.2'), - }, - { - Icon: MdHandshake, - children: t('list.3'), - }, - ]; +export const BenefitsSection: FC = ({ + data: { heading, description, list }, +}) => { + const { t } = useTranslation('~', { keyPrefix: 'dry-wash.landing' }); return ( - {t('heading')} - - {t('description')} - + {t(heading)} + {t(description)} - {listData.map((props, i) => ( - + {list.map((itemKey, i) => ( + + {t(itemKey)} + ))} diff --git a/src/components/landing/BenefitsSection/helper.ts b/src/components/landing/BenefitsSection/helper.ts new file mode 100644 index 0000000..66bed05 --- /dev/null +++ b/src/components/landing/BenefitsSection/helper.ts @@ -0,0 +1,11 @@ +import { IconType } from "react-icons"; +import { MdEco, MdMiscellaneousServices, MdPlace, MdHandshake } from "react-icons/md"; +import { ArrElement } from "../../../lib"; +import { BenefitsList } from "./types"; + +export const iconsMap: Record, IconType> = { + "benefits-section.list.0": MdEco, + "benefits-section.list.1": MdMiscellaneousServices, + "benefits-section.list.2": MdPlace, + "benefits-section.list.3": MdHandshake, +}; \ No newline at end of file diff --git a/src/components/landing/BenefitsSection/index.ts b/src/components/landing/BenefitsSection/index.ts index d10431b..f5cfc6d 100644 --- a/src/components/landing/BenefitsSection/index.ts +++ b/src/components/landing/BenefitsSection/index.ts @@ -1 +1,2 @@ +export type { BenefitsSectionProps } from './types'; export { BenefitsSection } from './BenefitsSection'; \ No newline at end of file diff --git a/src/components/landing/BenefitsSection/types.ts b/src/components/landing/BenefitsSection/types.ts new file mode 100644 index 0000000..4099a0d --- /dev/null +++ b/src/components/landing/BenefitsSection/types.ts @@ -0,0 +1,14 @@ +export type BenefitsList = [ + 'benefits-section.list.0', + 'benefits-section.list.1', + 'benefits-section.list.2', + 'benefits-section.list.3', +]; + +export type BenefitsSectionProps = { + data: { + heading: 'benefits-section.heading'; + description: 'benefits-section.description'; + list: BenefitsList; + }; +}; \ No newline at end of file diff --git a/src/components/landing/CtaButton/CtaButton.tsx b/src/components/landing/CtaButton/CtaButton.tsx index cf5df8d..d4769e7 100644 --- a/src/components/landing/CtaButton/CtaButton.tsx +++ b/src/components/landing/CtaButton/CtaButton.tsx @@ -6,7 +6,7 @@ import { ButtonProps, Button } from '@chakra-ui/react'; import { URLs } from '../../../__data__/urls'; export const CtaButton: FC = (props) => { - const { t } = useTranslation(); + const { t } = useTranslation('~', { keyPrefix: 'dry-wash.landing' }); return ( ); }; diff --git a/src/components/landing/HeroSection/HeroSection.tsx b/src/components/landing/HeroSection/HeroSection.tsx index 9ae61c9..cf19ebb 100644 --- a/src/components/landing/HeroSection/HeroSection.tsx +++ b/src/components/landing/HeroSection/HeroSection.tsx @@ -1,22 +1,21 @@ import React, { FC } from 'react'; import { useTranslation } from 'react-i18next'; -import { Box, Heading, Text, Center, VStack, BoxProps } from '@chakra-ui/react'; - +import { Box, Heading, Text, Center, VStack } from '@chakra-ui/react'; import { DemoVideoPosterImg } from '../../../assets/images'; import { CtaButton, SiteLogo, PageSection } from '../'; +import { HeroSectionProps } from './types'; -type HeroSectionProps = Pick; - -export const HeroSection: FC = ({ flexShrink }) => { - const { t } = useTranslation('~', { - keyPrefix: 'dry-wash.landing.hero-section', - }); +export const HeroSection: FC = ({ + data: { headline, description, video }, + flexShrink, +}) => { + const { t } = useTranslation('~', { keyPrefix: 'dry-wash.landing' }); return ( = ({ flexShrink }) => { color='white' __css={{ textWrap: 'balance' }} > - {t('headline')} + {t(headline)} - {t('description')} + {t(description)} diff --git a/src/components/landing/HeroSection/types.ts b/src/components/landing/HeroSection/types.ts new file mode 100644 index 0000000..ce2eb6a --- /dev/null +++ b/src/components/landing/HeroSection/types.ts @@ -0,0 +1,9 @@ +import { BoxProps } from "@chakra-ui/react"; + +export type HeroSectionProps = { + data: { + headline: 'hero-section.headline'; + description: 'hero-section.description'; + video: string; + }; +} & Pick; \ No newline at end of file diff --git a/src/components/landing/SocialProofSection/SocialProofSection.tsx b/src/components/landing/SocialProofSection/SocialProofSection.tsx index fd49911..c424f35 100644 --- a/src/components/landing/SocialProofSection/SocialProofSection.tsx +++ b/src/components/landing/SocialProofSection/SocialProofSection.tsx @@ -5,15 +5,16 @@ import { Heading, HStack } from '@chakra-ui/react'; import { CtaButton, PageSection } from '../'; import { ReviewsSlider } from './ReviewsSlider'; +import { SocialProofSectionProps } from './types'; -export const SocialProofSection: FC = () => { - const { t } = useTranslation('~', { - keyPrefix: 'dry-wash.landing.social-proof-section', - }); +export const SocialProofSection: FC = ({ + data: { heading }, +}) => { + const { t } = useTranslation('~', { keyPrefix: 'dry-wash.landing' }); return ( - {t('heading')} + {t(heading)} diff --git a/src/components/landing/SocialProofSection/index.ts b/src/components/landing/SocialProofSection/index.ts index e20037f..2bdd48c 100644 --- a/src/components/landing/SocialProofSection/index.ts +++ b/src/components/landing/SocialProofSection/index.ts @@ -1 +1,2 @@ +export type { SocialProofSectionProps } from './types'; export { SocialProofSection } from './SocialProofSection'; \ No newline at end of file diff --git a/src/components/landing/SocialProofSection/types.ts b/src/components/landing/SocialProofSection/types.ts new file mode 100644 index 0000000..539208d --- /dev/null +++ b/src/components/landing/SocialProofSection/types.ts @@ -0,0 +1,5 @@ +export type SocialProofSectionProps = { + data: { + heading: 'social-proof-section.heading'; + }; +}; \ No newline at end of file diff --git a/src/lib/types.ts b/src/lib/types.ts index b244840..a2dbcc5 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -1,3 +1,11 @@ +/** + * @example type Output = ArrElement<['a', 'b', 'c']>; + * // "a" | "b" | "c" + */ +export type ArrElement = ArrType extends readonly (infer ElementType)[] + ? ElementType + : never; + /** * @example type Output = Split<'a.b1' | 'a.b2', '.'>; * // ["a", "b1"] | ["a", "b2"] diff --git a/src/pages/landing/index.tsx b/src/pages/landing/index.tsx index 62155e7..bab11ee 100644 --- a/src/pages/landing/index.tsx +++ b/src/pages/landing/index.tsx @@ -1,13 +1,9 @@ import React, { FC } from 'react'; import { Container, VStack } from '@chakra-ui/react'; - -import { - BenefitsSection, - Footer, - HeroSection, - SocialProofSection, -} from '../../components/landing'; +import LandingSuccess from '../../../stubs/json/landing/landing-success.json'; +import { BenefitsSection, Footer, HeroSection, SocialProofSection } from '../../components/landing'; import { LandingThemeProvider } from '../../containers'; +import { isBenefitsSectionData, isSocialProofSectionData } from './types'; const Page: FC = () => { return ( @@ -21,10 +17,19 @@ const Page: FC = () => { centerContent > - + - - + {LandingSuccess.body.sections.map(({ type, ...data }, i) => { + if (isBenefitsSectionData(type, data)) { + return ; + } + if (isSocialProofSectionData(type, data)) { + return ; + } + })}