From 13874f70edddc297c711a6e4eae7753ae04fd746 Mon Sep 17 00:00:00 2001 From: Kate Latypova Date: Mon, 6 Feb 2023 18:20:57 +0300 Subject: [PATCH] feat: add Map-block --- .gitignore | 1 + .storybook/maps.ts | 6 + README.md | 8 + src/blocks/Map/Map.tsx | 15 ++ src/blocks/Map/README.md | 25 +++ src/blocks/Map/__stories__/Map.stories.tsx | 201 ++++++++++++++++++ src/blocks/Map/__stories__/data.json | 65 ++++++ src/blocks/Map/schema.ts | 20 ++ src/blocks/Media/Media.tsx | 63 +----- src/blocks/Media/schema.ts | 54 ++--- src/blocks/index.ts | 1 + src/components/Map/GoogleMap.tsx | 64 ++++++ src/components/Map/Map.scss | 25 +++ src/components/Map/Map.tsx | 23 ++ src/components/Map/README.md | 21 ++ src/components/Map/YMap/YMap.ts | 131 ++++++++++++ src/components/Map/YMap/YandexMap.tsx | 106 +++++++++ src/components/Map/YMap/YandexMapApiLoader.ts | 43 ++++ src/components/Map/YMap/i18n/en.json | 4 + src/components/Map/YMap/i18n/index.ts | 7 + src/components/Map/YMap/i18n/ru.json | 4 + .../Map/__stories__/ApiKeyInput.scss | 6 + .../Map/__stories__/ApiKeyInput.tsx | 54 +++++ .../Map/__stories__/Map.stories.tsx | 49 +++++ src/components/Map/__stories__/data.json | 33 +++ src/components/Map/helpers.ts | 3 + .../MediaBase/MediaBase.scss} | 5 +- src/components/MediaBase/MediaBase.tsx | 76 +++++++ .../MediaBase/MediaBaseContent.scss} | 2 +- .../MediaBase/MediaBaseContent.tsx} | 4 +- src/constructor-items.ts | 2 + src/containers/PageConstructor/Provider.tsx | 4 + src/context/mapsContext/mapsContext.ts | 27 +++ src/context/mapsContext/mapsProvider.tsx | 30 +++ src/context/mapsContext/useMap.ts | 8 + src/internal-typings/global.d.ts | 36 ++++ src/models/constructor-items/blocks.ts | 26 ++- src/models/constructor-items/common.ts | 31 +++ src/schema/index.ts | 2 + src/schema/validators/blocks.ts | 1 + src/schema/validators/common.ts | 48 +++++ src/utils/common.ts | 25 +++ src/utils/index.ts | 1 + 43 files changed, 1270 insertions(+), 90 deletions(-) create mode 100644 .storybook/maps.ts create mode 100644 src/blocks/Map/Map.tsx create mode 100644 src/blocks/Map/README.md create mode 100644 src/blocks/Map/__stories__/Map.stories.tsx create mode 100644 src/blocks/Map/__stories__/data.json create mode 100644 src/blocks/Map/schema.ts create mode 100644 src/components/Map/GoogleMap.tsx create mode 100644 src/components/Map/Map.scss create mode 100644 src/components/Map/Map.tsx create mode 100644 src/components/Map/README.md create mode 100644 src/components/Map/YMap/YMap.ts create mode 100644 src/components/Map/YMap/YandexMap.tsx create mode 100644 src/components/Map/YMap/YandexMapApiLoader.ts create mode 100644 src/components/Map/YMap/i18n/en.json create mode 100644 src/components/Map/YMap/i18n/index.ts create mode 100644 src/components/Map/YMap/i18n/ru.json create mode 100644 src/components/Map/__stories__/ApiKeyInput.scss create mode 100644 src/components/Map/__stories__/ApiKeyInput.tsx create mode 100644 src/components/Map/__stories__/Map.stories.tsx create mode 100644 src/components/Map/__stories__/data.json create mode 100644 src/components/Map/helpers.ts rename src/{blocks/Media/Media.scss => components/MediaBase/MediaBase.scss} (94%) create mode 100644 src/components/MediaBase/MediaBase.tsx rename src/{blocks/Media/MediaContent.scss => components/MediaBase/MediaBaseContent.scss} (87%) rename src/{blocks/Media/MediaContent.tsx => components/MediaBase/MediaBaseContent.tsx} (92%) create mode 100644 src/context/mapsContext/mapsContext.ts create mode 100644 src/context/mapsContext/mapsProvider.tsx create mode 100644 src/context/mapsContext/useMap.ts create mode 100644 src/utils/common.ts diff --git a/.gitignore b/.gitignore index 133e90e92..62b5977f0 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ node_modules *.tgz .env +.env.development diff --git a/.storybook/maps.ts b/.storybook/maps.ts new file mode 100644 index 000000000..92ef03789 --- /dev/null +++ b/.storybook/maps.ts @@ -0,0 +1,6 @@ +export const ymapApiKeyForStorybook = '536c9fe2-9365-40c1-aedc-f6f21817f82e'; + +export const scriptsSrc = { + yandex: 'https://api-maps.yandex.ru/2.1', + google: 'https://www.google.com/maps/embed/v1/place', +}; diff --git a/README.md b/README.md index bab88dd95..d76268a42 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ interface PageConstructorProviderProps { metrika?: Metrika; //Functions for sending analytics ssrConfig?: SSR; //A flag indicating that the code is run on the server size. theme?: 'light' | 'dark'; //Theme to render the page with. + mapsContext?: MapsContextType; //Params for map: apikey, type, scriptSrc, nonce } export interface PageContent extends Animatable { @@ -211,6 +212,13 @@ import {configure, Lang} from '@gravity-ui/page-constructor'; configure({lang: Lang.En}); ``` +### Maps + +To use maps, put the map type, scriptSrc and apiKey in field `mapContext` in `PageConstructorProvider`. + +You can define environment variables for dev-mode in .env.development file within project root. +`STORYBOOK_GMAP_API_KEY` - apiKey for google maps + ## Development ```bash diff --git a/src/blocks/Map/Map.tsx b/src/blocks/Map/Map.tsx new file mode 100644 index 000000000..e1ff161ce --- /dev/null +++ b/src/blocks/Map/Map.tsx @@ -0,0 +1,15 @@ +import React from 'react'; + +import {MapBlockProps} from '../../models'; +import Map from '../../components/Map/Map'; +import MediaBase from '../../components/MediaBase/MediaBase'; + +export const MapBlock = ({map, ...props}: MapBlockProps) => ( + + + + + +); + +export default MapBlock; diff --git a/src/blocks/Map/README.md b/src/blocks/Map/README.md new file mode 100644 index 000000000..22e864e12 --- /dev/null +++ b/src/blocks/Map/README.md @@ -0,0 +1,25 @@ +Map block + +`type: map-block` + +`title: string` — Title. + +`description: string` — Description. + +[`button: Button` — Button](?path=/story/information--common-types&viewMode=docs#button---button) + +[`map: Map` — Map description](?path=/story/components-map--y-map&viewMode=docs) + +`direction: 'media-content' | 'content-media'` — Relative position of map and content. + +`mobileDirection: 'media-content' | 'content-media'` - Relative position of map and content for touch + +`largeMedia?: boolean` — An image/video takes 8 columns. + +`disableShadow?: boolean` — Disable shadow for the block. + +`additionalInfo?: string` — Gray text (with YFM support) + +[`links?: Link[]` — An array with link objects](?path=/story/information--common-types&viewMode=docs#link---link) + +[`buttons?: Button[]` — An array with button objects](?path=/story/information--common-types&viewMode=docs#button---button) diff --git a/src/blocks/Map/__stories__/Map.stories.tsx b/src/blocks/Map/__stories__/Map.stories.tsx new file mode 100644 index 000000000..9e17a7e9d --- /dev/null +++ b/src/blocks/Map/__stories__/Map.stories.tsx @@ -0,0 +1,201 @@ +import React from 'react'; +import {Meta, Story} from '@storybook/react/types-6-0'; +import {yfmTransform} from '../../../../.storybook/utils'; +import {ButtonProps, LinkProps, MapBlockModel, MapBlockProps} from '../../../models'; +import MapBlock from '../Map'; +import {PageConstructor} from '../../../containers/PageConstructor'; +import {MapProvider, gmapApiKeyIdInLS} from '../../../context/mapsContext/mapsProvider'; +import {MapType} from '../../../context/mapsContext/mapsContext'; +import {ApiKeyInput} from '../../../components/Map/__stories__/ApiKeyInput'; +import {ymapApiKeyForStorybook, scriptsSrc} from '../../../../.storybook/maps'; + +import data from './data.json'; + +export default { + title: 'Blocks/Map', + component: MapBlock, + args: { + largeMedia: false, + mediaOnly: false, + size: 'l', + }, +} as Meta; + +const DefaultTemplate: Story = (args) => ( + + + +); + +const SizeTemplate: Story = (args) => ( + + + +); + +const DirectionTemplate: Story = (args) => ( + + + +); + +const GMAP_API_KEY = process.env.STORYBOOK_GMAP_API_KEY; + +const MapsTypesTemplate: Story = (args) => ( + <> + + + + + {!GMAP_API_KEY && ( +
+ +
+ )} + +
+ +); + +export const Default = DefaultTemplate.bind({}); +export const Size = SizeTemplate.bind({}); +export const Direction = DirectionTemplate.bind({}); +export const MapsTypes = MapsTypesTemplate.bind({}); + +const DefaultArgs = { + ...data.default.content, + title: data.common.title, + description: yfmTransform(data.common.description), + map: data.ymap, +}; + +Default.args = DefaultArgs as MapBlockProps; + +Size.args = DefaultArgs as MapBlockProps; +Direction.args = DefaultArgs as MapBlockProps; + +MapsTypes.args = DefaultArgs as MapBlockProps; diff --git a/src/blocks/Map/__stories__/data.json b/src/blocks/Map/__stories__/data.json new file mode 100644 index 000000000..4055065d4 --- /dev/null +++ b/src/blocks/Map/__stories__/data.json @@ -0,0 +1,65 @@ +{ + "common": { + "title": "Lorem ipsum dolor sit", + "description": "**Ut enim ad minim veniam** [quis nostrud](https://example.com) exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.", + "additionalInfo": "Duis aute irure dolor in [reprehenderit](https://example.com) n voluptate velit esse cillum dolore eu fugiat nulla pariatur.", + "links": [ + { + "url": "#", + "text": "Learn more", + "theme": "normal", + "arrow": true + } + ], + "buttons": [ + { + "text": "Button", + "theme": "action", + "url": "https://example.com" + }, + { + "text": "Button", + "theme": "outlined", + "url": "#" + } + ] + }, + "default": { + "content": { + "type": "map-block" + } + }, + "ymap": { + "zoom": 9, + "id": "common-places", + "center": [55.753994, 37.622093], + "markers": [ + { + "address": "Moscow Arbat", + "label": { + "preset": "islands#circleIcon" + } + }, + { + "coordinate": [55.733974, 37.587093], + "label": { + "iconCaption": "Yandex", + "iconColor": "#3caa3c" + } + } + ] + }, + "gmap": { + "zoom": 9, + "address": "Anthony Fokkerweg 1, 1059 CM Amsterdam" + }, + "size": { + "defaultMediaTitle": "Default map", + "largeMediaTitle": "Large map", + "mediaOnlyTitle": "map Only" + }, + "direction": { + "defaultDirectionTitle": "Default Direction", + "ReverseDirectionTitle": "Reverse Direction" + } +} diff --git a/src/blocks/Map/schema.ts b/src/blocks/Map/schema.ts new file mode 100644 index 000000000..dc2e3bb90 --- /dev/null +++ b/src/blocks/Map/schema.ts @@ -0,0 +1,20 @@ +import {MapProps} from '../../schema/validators/common'; +import {MediaBlockBaseProps} from '../Media/schema'; + +export const Map = { + type: 'object', + additionalProperties: false, + required: [], + properties: MapProps, +}; + +export const MapBlock = { + 'map-block': { + additionalProperties: false, + required: ['title', 'map'], + properties: { + ...MediaBlockBaseProps, + map: Map, + }, + }, +}; diff --git a/src/blocks/Media/Media.tsx b/src/blocks/Media/Media.tsx index 77fc8e75a..131d0659a 100644 --- a/src/blocks/Media/Media.tsx +++ b/src/blocks/Media/Media.tsx @@ -1,69 +1,24 @@ -import React, {useContext, useMemo, useState} from 'react'; +import React, {useContext, useState} from 'react'; -import {block, getThemedValue} from '../../utils'; -import {Grid, Row, Col, GridColumnSize} from '../../grid'; +import {getThemedValue} from '../../utils'; import {MediaBlockProps} from '../../models'; import Media from '../../components/Media/Media'; -import AnimateBlock from '../../components/AnimateBlock/AnimateBlock'; -import BlockHeader from '../../components/BlockHeader/BlockHeader'; -import MediaContent from './MediaContent'; +import MediaBase from '../../components/MediaBase/MediaBase'; import {ThemeValueContext} from '../../context/theme/ThemeValueContext'; -import './Media.scss'; - -const b = block('media-block'); - export const MediaBlock = (props: MediaBlockProps) => { - const { - media, - largeMedia, - direction = 'content-media', - mobileDirection = 'content-media', - animated, - mediaOnly, - disableShadow = false, - ...mediaContentProps - } = props; - const {title, description} = mediaContentProps; + const {media} = props; const [play, setPlay] = useState(false); const {themeValue: theme} = useContext(ThemeValueContext); const mediaThemed = getThemedValue(media, theme); - const mediaSizes = useMemo(() => { - return mediaOnly - ? {[GridColumnSize.All]: 12} - : {[GridColumnSize.Md]: largeMedia ? 8 : 6, [GridColumnSize.All]: 12}; - }, [mediaOnly, largeMedia]); - - const contentSizes = useMemo(() => { - return {[GridColumnSize.Md]: largeMedia ? 4 : 6, [GridColumnSize.All]: 12}; - }, [largeMedia]); - - const mediaContent = !mediaOnly && ; return ( - setPlay(true)} animate={animated}> - {mediaOnly && ( - - )} - - - - {mediaContent} - - -
- -
- -
-
-
+ setPlay(true)}> + + + + ); }; diff --git a/src/blocks/Media/schema.ts b/src/blocks/Media/schema.ts index b462f041b..99f1b669c 100644 --- a/src/blocks/Media/schema.ts +++ b/src/blocks/Media/schema.ts @@ -18,37 +18,41 @@ export const Media = { const MediaBlockContentProps = omit(ContentBase, ['text', 'theme']); +export const MediaBlockBaseProps = { + ...BlockBaseProps, + ...AnimatableProps, + ...MediaBlockContentProps, + description: { + type: 'string', + contentType: 'yfm', + }, + direction: { + type: 'string', + enum: mediaDirection, + }, + mobileDirection: { + type: 'string', + enum: mediaDirection, + }, + largeMedia: { + type: 'boolean', + }, + mediaOnly: { + type: 'boolean', + }, + disableShadow: { + type: 'boolean', + }, + button: ButtonBlock, +}; + export const MediaBlock = { 'media-block': { additionalProperties: false, required: ['title', 'media'], properties: { - ...BlockBaseProps, - ...AnimatableProps, - ...MediaBlockContentProps, - description: { - type: 'string', - contentType: 'yfm', - }, - direction: { - type: 'string', - enum: mediaDirection, - }, - mobileDirection: { - type: 'string', - enum: mediaDirection, - }, - largeMedia: { - type: 'boolean', - }, - mediaOnly: { - type: 'boolean', - }, - disableShadow: { - type: 'boolean', - }, + ...MediaBlockBaseProps, media: Media, - button: ButtonBlock, }, }, }; diff --git a/src/blocks/index.ts b/src/blocks/index.ts index 8c855a812..cea203198 100644 --- a/src/blocks/index.ts +++ b/src/blocks/index.ts @@ -3,6 +3,7 @@ export {default as CompaniesBlock} from './Companies/Companies'; export {default as SimpleBlock} from './Simple/Simple'; export {default as InfoBlock} from './Info/Info'; export {default as MediaBlock} from './Media/Media'; +export {default as MapBlock} from './Map/Map'; export {default as PreviewBlock} from './Preview/Preview'; export {default as SecurityBlock} from './Security/Security'; export {default as SliderBlock} from './Slider/Slider'; diff --git a/src/components/Map/GoogleMap.tsx b/src/components/Map/GoogleMap.tsx new file mode 100644 index 000000000..e46419de9 --- /dev/null +++ b/src/components/Map/GoogleMap.tsx @@ -0,0 +1,64 @@ +import React, {useContext, useEffect, useRef, useState, useMemo} from 'react'; +import _ from 'lodash'; + +import {block} from '../../utils'; +import {MapsContext} from '../../context/mapsContext/mapsContext'; +import {GMapProps} from '../../models'; +import {LocaleContext} from '../../context/localeContext/localeContext'; +import {MobileContext} from '../../context/mobileContext'; +import {getMapHeight} from './helpers'; + +const b = block('map'); + +function getScriptSrc(apiKey: string, scriptSrc: string, address: string, lang: 'ru' | 'en') { + return `${scriptSrc}?key=${apiKey}&language=${lang}&q=${encodeURI(address)}`; +} + +const GoogleMap: React.FC = (props) => { + const {address} = props; + const {apiKey, scriptSrc} = useContext(MapsContext); + const {lang = 'ru'} = useContext(LocaleContext); + const isMobile = useContext(MobileContext); + + const [height, setHeight] = useState(undefined); + const ref = useRef(null); + const src = useMemo( + () => getScriptSrc(apiKey, scriptSrc, address, lang), + [apiKey, scriptSrc, address, lang], + ); + + useEffect(() => { + const updateSize = _.debounce(() => { + if (ref.current) { + setHeight(Math.round(getMapHeight(ref.current.offsetWidth, isMobile))); + } + }, 100); + + updateSize(); + window.addEventListener('resize', updateSize); + + return () => { + window.removeEventListener('resize', updateSize); + }; + }, [isMobile]); + + if (!apiKey || !address) { + return null; + } + + return ( +