diff --git a/components/Card/DesktopCard/DesktopCard.tsx b/components/Card/DesktopCard/index.tsx similarity index 51% rename from components/Card/DesktopCard/DesktopCard.tsx rename to components/Card/DesktopCard/index.tsx index d2751952..2bb3612a 100644 --- a/components/Card/DesktopCard/DesktopCard.tsx +++ b/components/Card/DesktopCard/index.tsx @@ -1,17 +1,14 @@ -import React from 'react'; -import { Card } from 'components/Card/Card'; +import React, { ReactNode } from 'react'; import { Close } from 'shared/UI/Close'; -import { ContentConfig, MapItemType } from 'types/Content.types'; import styles from './DesktopCard.module.css'; interface Props { - contentConfig: ContentConfig; popupId?: string; - popupType: MapItemType | null; + children: ReactNode; closePopup: () => void; } -export function DesktopCard({ contentConfig, popupId, popupType, closePopup }: Props) { +export function DesktopCard({ popupId, children, closePopup }: Props) { if (!popupId) { return <>; } @@ -21,7 +18,7 @@ export function DesktopCard({ contentConfig, popupId, popupType, closePopup }: P
- + {children} ); } diff --git a/components/Card/MobileCard.tsx b/components/Card/MobileCard.tsx deleted file mode 100644 index 52ea9413..00000000 --- a/components/Card/MobileCard.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { SheetModal } from 'ekb'; -import { Card } from 'components/Card/Card'; -import { ContentConfig, MapItemType } from 'types/Content.types'; - -interface Props { - contentConfig: ContentConfig; - popupId?: string; - popupType: MapItemType | null; - closePopup: () => void; -} - -export function MobileCard({ contentConfig, popupId, popupType, closePopup }: Props) { - return ( - - - - ); -} diff --git a/components/Card/components/CardActions.tsx b/components/Card/components/CardActions.tsx new file mode 100644 index 00000000..ea6c5a27 --- /dev/null +++ b/components/Card/components/CardActions.tsx @@ -0,0 +1,54 @@ +import React, { useMemo } from 'react'; +import { Button, ButtonSize, ButtonType, Icon, IconType } from 'ekb'; +import { useCopyHref } from 'shared/helpers/useCopyHref'; + +type Props = { + coordinates?: [number, number] | number[]; +}; + +const COPY_RESET_TIMEOUT = 2000; + +export function CardActions({ coordinates }: Props) { + const { isCopied: isLinkCopied, onCopy: onCopyLink } = useCopyHref( + window.location.href, + COPY_RESET_TIMEOUT, + ); + + const coordsString = useMemo(() => { + if (!coordinates) { + return null; + } + + const coords = Array.isArray(coordinates[0]) ? coordinates[0] : coordinates; + + return `${coords[0]?.toFixed(6)}, ${coords[1]?.toFixed(6)}`; + }, [coordinates]); + + const { isCopied: isCoordsCopied, onCopy: onCopyCoords } = useCopyHref( + coordsString, + COPY_RESET_TIMEOUT, + ); + + return ( + <> + {coordsString && ( + + )} + + + ); +} diff --git a/components/Card/components/ConstructionInfo/ConstructionInfo.tsx b/components/Card/components/ConstructionInfo/ConstructionInfo.tsx deleted file mode 100644 index d45cf49e..00000000 --- a/components/Card/components/ConstructionInfo/ConstructionInfo.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import React, { useMemo } from 'react'; -import { getYearNameByValue } from 'shared/helpers/getYearNameByValue'; -import { Info } from 'components/Card/components/Info/Info'; - -const YEAR_RE = /\d{4}/; - -type ConstructionInfoProps = { - date: string; -}; - -export function ConstructionInfo({ date }: ConstructionInfoProps) { - const constructionDateInfo = useMemo(() => { - const result = [ - { - name: 'Когда построили', - text: date, - }, - ]; - - const constructionYearMatch = date.match(YEAR_RE); - - if (constructionYearMatch) { - const constructionYear = Number(constructionYearMatch[0]); - const age = new Date().getFullYear() - Number(constructionYear); - - result.push({ - name: 'Возраст здания', - text: `${String(age)} ${getYearNameByValue(age)}`, - }); - } - - return result; - }, [date]); - - return ( - - ); -} diff --git a/components/Card/components/Header/Header.module.css b/components/Card/components/Header/Header.module.css deleted file mode 100644 index c443ba4c..00000000 --- a/components/Card/components/Header/Header.module.css +++ /dev/null @@ -1,29 +0,0 @@ -.header { - display: flex; - flex-wrap: wrap; - column-gap: 4px; -} - -.header__title { - margin: 12px 0 0; - font-size: 30px; - font-weight: 500; - line-height: 1; -} - -.header__description { - margin: 4px 0 0; - font-size: 14px; - font-weight: 400; - line-height: 20px; -} - -@media screen and (width >= 1150px) { - .header__title { - font-size: 32px; - } - - .header__description { - font-size: 16px; - } -} diff --git a/components/Card/components/Header/Header.tsx b/components/Card/components/Header/Header.tsx deleted file mode 100644 index d63477e6..00000000 --- a/components/Card/components/Header/Header.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import React, { useMemo } from 'react'; -import { Button, ButtonSize, ButtonType } from 'ekb'; -import { Icon } from 'shared/UI/Icons'; -import { IconType } from 'shared/UI/Icons/Icons.types'; -import { useCopyHref } from 'shared/helpers/useCopyHref'; -import styles from './Header.module.css'; - -export type HeaderProps = { - coordinates?: [number, number] | number[]; - title?: string; - description?: string; -}; - -const COPY_RESET_TIMEOUT = 2000; - -export function Header({ coordinates, title, description }: HeaderProps) { - const { isCopied: isLinkCopied, onCopy: onCopyLink } = useCopyHref( - window.location.href, - COPY_RESET_TIMEOUT, - ); - - const coordsString = useMemo(() => { - if (!coordinates) { - return null; - } - - const coords = Array.isArray(coordinates[0]) ? coordinates[0] : coordinates; - - return `${coords[0]?.toFixed(6)}, ${coords[1]?.toFixed(6)}`; - }, [coordinates]); - const { isCopied: isCoordsCopied, onCopy: onCopyCoords } = useCopyHref( - coordsString, - COPY_RESET_TIMEOUT, - ); - - return ( - <> -
- {coordsString && ( - - )} - -
-

{title}

- {description &&

{description}

} - - ); -} diff --git a/components/Card/components/Section/Section.module.css b/components/Card/components/Section/Section.module.css deleted file mode 100644 index a47616fe..00000000 --- a/components/Card/components/Section/Section.module.css +++ /dev/null @@ -1,11 +0,0 @@ -.block { - margin-top: 16px; - padding-top: 16px; - border-top: 1px solid rgba(256, 256, 256, 0.16); -} - -.block_inline { - display: flex; - flex-wrap: wrap; - gap: 8px; -} diff --git a/components/Card/components/Section/Section.tsx b/components/Card/components/Section/Section.tsx deleted file mode 100644 index 7c82e9cf..00000000 --- a/components/Card/components/Section/Section.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import React from 'react'; - -import styles from './Section.module.css'; - -export function Section({ children }: React.PropsWithChildren) { - return
{children}
; -} diff --git a/components/Card/components/Sources/Sources.tsx b/components/Card/components/Sources/Sources.tsx index 812c60ed..28919488 100644 --- a/components/Card/components/Sources/Sources.tsx +++ b/components/Card/components/Sources/Sources.tsx @@ -6,7 +6,7 @@ import styles from './Sources.module.css'; export function Sources({ sources }: { sources: SourceInfo[] }) { return (
-

Источники

+ {/*

Источники

*/}
    {sources.map(({ link, name, data }) => { return ( diff --git a/components/Card/index.ts b/components/Card/index.ts deleted file mode 100644 index 984b6544..00000000 --- a/components/Card/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { DesktopCard } from './DesktopCard/DesktopCard'; -export { MobileCard } from './MobileCard'; diff --git a/components/Card/Card.tsx b/components/Card/index.tsx similarity index 99% rename from components/Card/Card.tsx rename to components/Card/index.tsx index cdbdbe84..d93f158c 100644 --- a/components/Card/Card.tsx +++ b/components/Card/index.tsx @@ -25,7 +25,6 @@ export function Card({ contentConfig, popupId, popupType }: Props) { } setLoading(true); - const requestFunction = contentConfig[popupType].oneItemRequest; const data = await requestFunction(popupId); diff --git a/components/Filters/FilterGrid.tsx b/components/Filters/FilterGrid.tsx new file mode 100644 index 00000000..2396f632 --- /dev/null +++ b/components/Filters/FilterGrid.tsx @@ -0,0 +1,53 @@ +import React, { ComponentProps, useCallback, useEffect, useState } from 'react'; +import { Checkbox, ListGrid, ListGridItem } from 'ekb'; + +export type IFilterGridItem = Partial> & { + type: string; + color?: string; +}; + +interface Props { + selectedByDefault?: string[]; + items?: IFilterGridItem[]; + onChange?: (state: string[]) => void; +} + +export function FilterGrid({ items, onChange, selectedByDefault = [] }: Props) { + const [selected, setSelected] = useState(selectedByDefault); + + const toggle = useCallback( + (type: string) => { + if (selected.includes(type)) { + setSelected(selected.filter((s) => s !== type)); + } else { + setSelected(selected.concat(type)); + } + }, + [selected], + ); + + useEffect(() => { + onChange?.(selected); + }, [onChange, selected]); + + return ( + + {items.map(({ type, subTitle, description, color }) => ( + toggle(type)} + /> + } + > + {type} + + ))} + + ); +} diff --git a/components/Filters/index.ts b/components/Filters/index.ts deleted file mode 100644 index 194f6207..00000000 --- a/components/Filters/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { Filters } from './Filters'; diff --git a/features/App/Content.config.ts b/features/App/Content.config.ts index 458a0a10..22019f10 100644 --- a/features/App/Content.config.ts +++ b/features/App/Content.config.ts @@ -3,13 +3,13 @@ import { designCode } from 'features/DesignCode/designCode'; import { dtp } from 'features/DTP/dtp'; import { okn } from 'features/OKN/okn'; import { lines } from 'features/Lines/lines'; -import { LinesCardContent } from 'features/Lines/CardContent/CardContent'; -import { QuarterCardContent } from 'features/Quarter/CardContent/CardContent'; +import { LinesCardContent } from 'features/Lines/LinesCardContent'; +import { QuarterCardContent } from 'features/Quarter/QuarterCardContent'; import { quarter } from 'features/Quarter/quarter'; -import { DesignCodeCardContent } from 'features/DesignCode/CardContent/CardContent'; -import { DTPCardContent } from 'features/DTP/CardContent/CardContent'; -import { HousesCardContent } from 'features/Buildings/CardContent/CardContent'; -import { OKNCardContent } from 'features/OKN/CardContent'; +import { DesignCodeCardContent } from 'features/DesignCode/DesignCodeCard'; +import { DTPCardContent } from 'features/DTP/DtpCardContent'; +import { HousesCardContent } from 'features/Buildings/BuildingCard'; +import { OKNCardContent } from 'features/OKN/OknCardContent'; export const CONTENTS_CONFIG: ContentConfig = { [MapItemType.Houses]: { diff --git a/features/App/Filters.config.tsx b/features/App/Filters.config.tsx index 5d826c60..6fb229e4 100644 --- a/features/App/Filters.config.tsx +++ b/features/App/Filters.config.tsx @@ -1,15 +1,13 @@ import { DTPFilter } from 'features/DTP/Filter/DTPFilter'; -import { DesignCodeFilter } from 'features/DesignCode/Filter/DesignCodeFilter'; import { HouseAgeFilter } from 'features/HouseAge/HouseAgeFilter'; import { HouseFloorFilter } from 'features/HouseFloor/HouseFloorFilter'; import { HouseWearTearFilter } from 'features/HouseWearTear/HouseWearTearFilter'; -import { LinesFilter } from 'features/Lines/Filter/LinesFilter'; import { OknFilter } from 'features/OKN/Filter/OknFilter'; -import { QuarterFilter } from 'features/Quarter/Filter/QuarterFilter'; +import { QuarterFilter } from 'features/Quarter/QuarterFilter'; import { SOURCES_BY_TYPE } from 'constants/sources'; import { SourceType } from 'types/Sources.types'; import { FilterConfig, FilterType } from 'types/Filters.types'; -import { FacadeFilter } from 'features/Facade/Filter/FacadeFilter'; +import { FacadeFilter } from 'features/Facade/FacadeFilter'; export const FILTERS_CONFIG: FilterConfig = { [FilterType.HouseAge]: { @@ -38,7 +36,6 @@ export const FILTERS_CONFIG: FilterConfig = { }, [FilterType.DesignCode]: { title: '«Дизайн-код Екатеринбурга»', - component: , source: [SOURCES_BY_TYPE[SourceType.design_objects_map]], isVerified: true, }, @@ -50,7 +47,6 @@ export const FILTERS_CONFIG: FilterConfig = { }, [FilterType.Line]: { title: 'Туристические маршруты', - component: , isVerified: true, }, [FilterType.Quarter]: { diff --git a/features/App/Sidebars.tsx b/features/App/Sidebars.tsx index f4178cce..d08d3b8c 100644 --- a/features/App/Sidebars.tsx +++ b/features/App/Sidebars.tsx @@ -5,13 +5,14 @@ import { toggleData } from 'state/features/dataLayers'; import { activeFilterSelector } from 'state/features/selectors'; import { FilterType } from 'types/Filters.types'; import { useIsDesktop } from 'shared/helpers/isDesktop'; -import { DesktopCard, MobileCard } from 'components/Card'; +import { Card } from 'components/Card'; +import { DesktopCard } from 'components/Card/DesktopCard'; import { LeftSidebar } from 'components/LeftSidebar'; import { RightSidebar } from 'components/RightSidebar'; import { AboutProjectModal } from 'features/About/AboutProjectModal'; import { MobileAboutProject } from 'features/About/MobileAboutProject'; import { MapContext } from 'features/Map/providers/MapProvider'; -import { Filters } from 'components/Filters'; +import { Filters } from 'features/Filters/Filters'; import { CONTENTS_CONFIG } from './Content.config'; import { FILTERS_CONFIG } from './Filters.config'; @@ -40,12 +41,14 @@ export function Sidebars() { /> ); + const card = ; + if (isDesktop) { return ( <> {filter} - + {card} @@ -57,7 +60,13 @@ export function Sidebars() { {filter} - + + {card} + ); diff --git a/features/Buildings/CardContent/CardContent.tsx b/features/Buildings/BuildingCard.tsx similarity index 53% rename from features/Buildings/CardContent/CardContent.tsx rename to features/Buildings/BuildingCard.tsx index bac2ac41..821067c0 100644 --- a/features/Buildings/CardContent/CardContent.tsx +++ b/features/Buildings/BuildingCard.tsx @@ -1,30 +1,24 @@ import { useContext, useEffect, useMemo, useState } from 'react'; import { useMap } from 'react-map-gl'; -import { Tag } from 'ekb'; +import { Card, Tag } from 'ekb'; import { HouseObject } from 'features/Buildings/houseBase'; import { MapContext } from 'features/Map/providers/MapProvider'; import { usePopup } from 'features/Map/providers/usePopup'; -import { ConstructionInfo } from 'components/Card/components/ConstructionInfo/ConstructionInfo'; -import Facade from 'features/Facade/CardContent/Facade'; -import { Header } from 'components/Card/components/Header/Header'; -import { Info } from 'components/Card/components/Info/Info'; -import { Section } from 'components/Card/components/Section/Section'; +import { CardActions } from 'components/Card/components/CardActions'; import { Sources } from 'components/Card/components/Sources/Sources'; -import { FeedbackButton } from 'features/FeedbackButton/FeedbackButton'; -import { FilterLoader } from 'components/Filters/FilterLoader'; +import { getYearNameByValue } from 'shared/helpers/getYearNameByValue'; import { getLatLngFromHash } from 'shared/helpers/hash'; -import { useIsDesktop } from 'shared/helpers/isDesktop'; import { SOURCES_BY_TYPE } from 'constants/sources'; import { SourceType } from 'types/Sources.types'; import facades from 'public/ekb-facades.json'; import HealthProgress from 'features/HouseWearTear/HealthProgress/HealthProgress'; -import styles from './CardContent.module.css'; +import { FeedbackButton } from 'features/FeedbackButton/FeedbackButton'; +import { DownloadButton } from 'features/Facade/DownloadButton/DownloadButton'; export function HousesCardContent() { const { popupId } = usePopup(); const { ekbMap } = useMap(); const { loading } = useContext(MapContext); - const isDesktop = useIsDesktop(); const [placemark, setPlacemark] = useState(null); @@ -96,35 +90,38 @@ export function HousesCardContent() { const result = []; if (placemark?.attributes?.Management_company) { result.push({ - name: 'Управляющая компания', - text: placemark?.attributes?.Management_company, + title: 'Управляющая компания', + value: placemark?.attributes?.Management_company, }); } if (placemark?.attributes?.WearAndTear) { result.push({ - name: 'Износ', - text: `${placemark?.attributes?.WearAndTear} %`, - content: ( - + title: 'Износ', + value: ( + // TODO: refactor +
    + {placemark?.attributes?.WearAndTear} % + +
    ), }); } if (placemark?.attributes?.Series) { result.push({ - name: 'Серия дома', - text: placemark?.attributes?.Series, + title: 'Серия дома', + value: placemark?.attributes?.Series, }); } if (placemark?.attributes?.Floors) { result.push({ - name: 'Количество этажей', - text: placemark?.attributes?.Floors, + title: 'Количество этажей', + value: placemark?.attributes?.Floors, }); } @@ -137,6 +134,36 @@ export function HousesCardContent() { isEmergency, ]); + const constructionDateInfo = useMemo(() => { + const date = String(placemark?.attributes.Year); + const result = [ + { + type: 'value', + title: 'Когда построили', + value: date, + }, + ]; + + const constructionYearMatch = date.match(/\d{4}/); + + if (constructionYearMatch) { + const constructionYear = Number(constructionYearMatch[0]); + const age = new Date().getFullYear() - Number(constructionYear); + + result.push({ + type: 'value', + title: 'Возраст здания', + value: `${String(age)} ${getYearNameByValue(age)}`, + }); + } + + if (result.length && aboutHouse.length) { + return [{ type: 'divider' }].concat(result as any); + } + + return []; + }, [aboutHouse.length, placemark?.attributes.Year]); + const defaultSources = [ SOURCES_BY_TYPE[SourceType.osm], SOURCES_BY_TYPE[SourceType.howoldthishouse], @@ -144,12 +171,18 @@ export function HousesCardContent() { SOURCES_BY_TYPE[SourceType.domaekb], ]; - if (loading) { - return ( -
    - -
    - ); + const facade = facades[placemark?.attributes?.osmId]; + const facadesSection = []; + if (facade) { + defaultSources.push(SOURCES_BY_TYPE[SourceType.ekaterinburgdesign]); + if (aboutHouse.length || constructionDateInfo.length) { + facadesSection.push({ type: 'divider' }); + } + facadesSection.push({ + type: 'section', + title: 'Дизайн-код фасада', + value: , + }); } if (!placemark?.attributes) { @@ -157,44 +190,29 @@ export function HousesCardContent() { } return ( -
    -
    - {(isEmergency || aboutHouse?.length > 0) && ( -
    - {isEmergency && ( -
    - {placemark?.attributes.Condition} -
    - )} - -
    - )} - {placemark?.attributes.Year && ( -
    - -
    - )} - - {facades[placemark?.attributes?.osmId] && ( -
    - -
    - )} -
    - -
    -
    - -
    -
    + } + title={placemark?.attributes.Address} + description={ + isEmergency ? ( + + {placemark?.attributes.Condition} + + ) : undefined + } + blocks={[ + ...aboutHouse.map((item) => ({ ...item, type: 'value' })), + ...constructionDateInfo, + ...facadesSection, + { type: 'divider' }, + { + type: 'section', + title: 'Источники', + value: , + }, + ]} + footerActions={} + /> ); } diff --git a/features/Buildings/CardContent/CardContent.module.css b/features/Buildings/CardContent/CardContent.module.css deleted file mode 100644 index 4b149b29..00000000 --- a/features/Buildings/CardContent/CardContent.module.css +++ /dev/null @@ -1,14 +0,0 @@ -.popup { - border-top-left-radius: inherit; - border-top-right-radius: inherit; - padding: 16px 8px 16px 16px; -} -.popup_mobile { - border-top-left-radius: inherit; - border-top-right-radius: inherit; - padding: 16px; -} - -.popup__emergencyLabel { - margin-bottom: 12px; -} diff --git a/features/Buildings/Filter/HouseBaseFilter.tsx b/features/Buildings/HouseBaseFilter.tsx similarity index 100% rename from features/Buildings/Filter/HouseBaseFilter.tsx rename to features/Buildings/HouseBaseFilter.tsx diff --git a/features/DTP/CardContent/CardContent.module.css b/features/DTP/CardContent/CardContent.module.css deleted file mode 100644 index 8a0279ac..00000000 --- a/features/DTP/CardContent/CardContent.module.css +++ /dev/null @@ -1,14 +0,0 @@ -.popup { - border-top-left-radius: inherit; - border-top-right-radius: inherit; - padding: 16px 8px 16px 16px; -} - -.popup__extraText { - margin: 8px 16px 0 0; - font-style: normal; - font-weight: 400; - line-height: 20px; - font-size: 16px; - color: #9baac3; -} diff --git a/features/DTP/CardContent/components/Participants/Participants.module.css b/features/DTP/CardContent/components/Participants/Participants.module.css index 29e52226..143c420d 100644 --- a/features/DTP/CardContent/components/Participants/Participants.module.css +++ b/features/DTP/CardContent/components/Participants/Participants.module.css @@ -1,12 +1,7 @@ -.participants__title { - margin: 0; -} - -.participants__filter { - margin-top: 6px; - display: flex; - flex-direction: row; - gap: 8px; +.participants__section { + margin-top: 16px; + padding-top: 16px; + border-top: 1px solid rgba(256, 256, 256, 0.16); } .participants__participants { diff --git a/features/DTP/CardContent/components/Participants/Participants.tsx b/features/DTP/CardContent/components/Participants/Participants.tsx index 1c8e6d76..48643750 100644 --- a/features/DTP/CardContent/components/Participants/Participants.tsx +++ b/features/DTP/CardContent/components/Participants/Participants.tsx @@ -1,6 +1,5 @@ import React from 'react'; import { Tag } from 'ekb'; -import { Section } from 'components/Card/components/Section/Section'; import { DTPParticipant, DTPVehicle } from 'features/DTP/dtp'; import { healthStatusToType, HEALTH_STATUS_COLOR, Participant } from './Participant/Participant'; import { ParticipantsProps, HealthStatusType } from './Participants.types'; @@ -38,72 +37,63 @@ export function getVehicleName(vehicle: DTPVehicle) { export function Participants({ participants, vehicles }: ParticipantsProps) { return ( -
    -

    Участники

    -
    - <> - {vehicles.map((vehicle, i) => { - const driverIndex = vehicle.participants.findIndex( - (participant) => participant.role === 'Водитель', - ); +
    + <> + {vehicles.map((vehicle, i) => { + const driverIndex = vehicle.participants.findIndex( + (participant) => participant.role === 'Водитель', + ); - let driver: DTPParticipant; - let driverHealthStatus: HealthStatusType; + let driver: DTPParticipant; + let driverHealthStatus: HealthStatusType; - if (driverIndex !== -1) { - driver = vehicle.participants[driverIndex]; - driverHealthStatus = healthStatusToType(driver.health_status); - } + if (driverIndex !== -1) { + driver = vehicle.participants[driverIndex]; + driverHealthStatus = healthStatusToType(driver.health_status); + } - const restParticipants = vehicle.participants.filter( - (participant) => participant.role !== 'Водитель', - ); + const restParticipants = vehicle.participants.filter( + (participant) => participant.role !== 'Водитель', + ); - const car = getVehicleName(vehicle); + const car = getVehicleName(vehicle); - return ( -
    -
    -
    -
    -
    - {car} -
    -
    - {vehicle.color} -
    + return ( +
    +
    +
    +
    +
    {car}
    +
    + {vehicle.color}
    - {driver && ( - - {driverHealthStatus} - - )}
    {driver && ( -
    - -
    + + {driverHealthStatus} + )}
    - {restParticipants.map((participant, i) => ( -
    - -
    - ))} + {driver && ( +
    + +
    + )}
    - ); - })} - {participants.map((participant, i) => ( -
    - + {restParticipants.map((participant, i) => ( +
    + +
    + ))}
    - ))} - -
    + ); + })} + {participants.map((participant, i) => ( +
    + +
    + ))} +
    ); } diff --git a/features/DTP/DTP.constants.ts b/features/DTP/DTP.constants.ts index fa2bfa8e..42824815 100644 --- a/features/DTP/DTP.constants.ts +++ b/features/DTP/DTP.constants.ts @@ -1,12 +1,11 @@ import { range } from 'lodash'; -import { HistogramDataWithoutValues } from 'ekb'; -import { IconType } from 'shared/UI/Icons/Icons.types'; +import { HistogramDataWithoutValues, IconType } from 'ekb'; import { DtpParticipantType, DtpSeverityType } from './types'; export type ParticipantConfig = Record; export const MIN_DTP_YEAR = 2015; -export const CURRENT_YEAR = new Date().getFullYear(); +export const MAX_DTP_YEAR = 2023; export const DTP_PARTICIPANT_CONFIG: ParticipantConfig = { [DtpParticipantType.Children]: { @@ -31,19 +30,13 @@ export const DTP_PARTICIPANT_CONFIG: ParticipantConfig = { }, }; -export const SEVERITY_CONFIG = { - [DtpSeverityType.Light]: { - color: '#36ccaa', - }, - [DtpSeverityType.Heavy]: { - color: '#fdcf4e', - }, - [DtpSeverityType.WithDead]: { - color: '#ff0000', - }, +export const SEVERITY_CONFIG_COLOR = { + [DtpSeverityType.Light]: '#36ccaa', + [DtpSeverityType.Heavy]: '#fdcf4e', + [DtpSeverityType.WithDead]: '#ff0000', }; -export const DTP_YEARS_RANGE = range(MIN_DTP_YEAR, CURRENT_YEAR); +export const DTP_YEARS_RANGE = range(MIN_DTP_YEAR, MAX_DTP_YEAR); export const YEARS_FILTERS_DATA: HistogramDataWithoutValues = DTP_YEARS_RANGE.map((year) => ({ from: year, to: year + 1, diff --git a/features/DTP/CardContent/CardContent.tsx b/features/DTP/DtpCardContent.tsx similarity index 54% rename from features/DTP/CardContent/CardContent.tsx rename to features/DTP/DtpCardContent.tsx index 1352db08..beba3fb8 100644 --- a/features/DTP/CardContent/CardContent.tsx +++ b/features/DTP/DtpCardContent.tsx @@ -1,15 +1,11 @@ import React, { useMemo } from 'react'; - -import { Section } from 'components/Card/components/Section/Section'; -import { Header } from 'components/Card/components/Header/Header'; -import { Info } from 'components/Card/components/Info/Info'; +import { Card } from 'ekb'; +import { CardActions } from 'components/Card/components/CardActions'; import { Sources } from 'components/Card/components/Sources/Sources'; -import { InfoProps } from 'components/Card/components/Info/Info.types'; import { SourceType } from 'types/Sources.types'; import { SOURCES_BY_TYPE } from 'constants/sources'; -import { DTPObject } from '../dtp'; -import styles from './CardContent.module.css'; -import { Participants } from './components/Participants/Participants'; +import { DTPObject } from './dtp'; +import { Participants } from './CardContent/components/Participants/Participants'; export type DTPCardContentProps = { placemark: DTPObject; @@ -46,12 +42,13 @@ export function DTPCardContent({ placemark }: DTPCardContentProps) { }, [placemark?.properties.datetime]); const environment = useMemo(() => { - const result: InfoProps['infos'] = []; + const result = []; if (placemark?.properties.light) { result.push({ - name: 'Время суток', - text: placemark.properties.light, + type: 'value', + title: 'Время суток', + value: placemark.properties.light, }); } @@ -63,8 +60,9 @@ export function DTPCardContent({ placemark }: DTPCardContentProps) { ].join(', '); result.push({ - name: 'Погода', - text: joinedConditions, + type: 'value', + title: 'Погода', + value: joinedConditions, }); } @@ -76,8 +74,9 @@ export function DTPCardContent({ placemark }: DTPCardContentProps) { ].join(', '); result.push({ - name: 'Дорожные условия', - text: joinedConditions, + type: 'value', + title: 'Дорожные условия', + value: joinedConditions, }); } @@ -88,33 +87,40 @@ export function DTPCardContent({ placemark }: DTPCardContentProps) { placemark?.properties.light, ]); - return placemark?.properties ? ( -
    -
    - {date &&

    {date}

    } - {placemark?.properties.address && ( -
    - {placemark?.properties.address} -
    - )} -
    - -
    - {placemark?.properties.participants_count && ( -
    - -
    - )} -
    - -
    -
    - ) : null; + if (!placemark?.properties) { + return null; + } + + return ( + } + title={title} + description={description} + additionalInfo={[date, placemark?.properties.address].filter(Boolean)} + blocks={[ + ...environment, + ...(placemark?.properties?.participants_count + ? [ + { type: 'divider' }, + { + type: 'section', + title: 'Участники', + value: ( + + ), + }, + ] + : undefined), + { type: 'divider' }, + { + type: 'section', + title: 'Источники', + value: , + }, + ]} + /> + ); } diff --git a/features/DTP/DtpSource.tsx b/features/DTP/DtpSource.tsx index d9eed3e0..16bd7759 100644 --- a/features/DTP/DtpSource.tsx +++ b/features/DTP/DtpSource.tsx @@ -4,7 +4,7 @@ import type { CircleLayer, HeatmapLayer } from 'react-map-gl'; import { useSelector } from 'react-redux'; import { activeFilterSelector, activeFilterParamsSelector } from 'state/features/selectors'; import { FilterType } from 'types/Filters.types'; -import { SEVERITY_CONFIG } from 'features/DTP/DTP.constants'; +import { SEVERITY_CONFIG_COLOR } from 'features/DTP/DTP.constants'; import { MapItemType } from 'types/Content.types'; import { MAX_ZOOM, MIN_ZOOM } from 'constants/map'; import dtp from 'public/ekb-dtp.json'; @@ -24,35 +24,36 @@ export function DtpSource() { if (activeFilter !== FilterType.DTP || !activeFilterParams) { return null; } - const data = { ...dtp, features: dtp.features.filter((feature) => { - const { severity } = feature.properties; - - const matchSeverity = activeFilterParams.severity.includes(severity); + const matchSeverity = activeFilterParams?.dtp?.dtpSeverity?.includes( + feature.properties.severity, + ); const matchYear = - feature.properties.year >= activeFilterParams.years.from && - feature.properties.year < activeFilterParams.years.to; + feature.properties.year >= activeFilterParams?.dtp?.range?.min && + feature.properties.year < activeFilterParams?.dtp?.range?.max; - const matchParticipants = activeFilterParams.participants.some((c) => - feature.properties.participant_categories.includes(c), + const matchParticipants = activeFilterParams?.dtp?.participants?.some( + (c) => feature.properties.participant_categories?.includes(c), ); return matchSeverity && matchParticipants && matchYear; }), }; - const colors = Object.entries(SEVERITY_CONFIG).map(([severity, { color }]) => [ - ['==', ['get', 'severity'], severity], - color, - ]); + const colors = + activeFilterParams?.dtp?.dtpSeverity?.map((severity) => [ + ['==', ['get', 'severity'], severity], + SEVERITY_CONFIG_COLOR[severity], + ]) || []; - const strokeColors = Object.entries(SEVERITY_CONFIG).map(([severity]) => [ - ['==', ['get', 'severity'], severity], - '#000', - ]); + const strokeColors = + activeFilterParams?.dtp?.dtpSeverity?.map((severity) => [ + ['==', ['get', 'severity'], severity], + '#000', + ]) || []; const layerStyle: CircleLayer = { id: DTP_LAYER_ID, @@ -104,8 +105,8 @@ export function DtpSource() { return ( - - + {activeFilterParams?.dtp?.dtpSeverity && } + {activeFilterParams?.dtp?.dtpSeverity && } ); } diff --git a/features/DTP/Filter/DTPFilter.module.css b/features/DTP/Filter/DTPFilter.module.css index 9beb6982..cc26de49 100644 --- a/features/DTP/Filter/DTPFilter.module.css +++ b/features/DTP/Filter/DTPFilter.module.css @@ -2,21 +2,3 @@ font-size: 18px; margin: 11px 0 8px; } - -.DTPFilter__checkboxContent { - width: 100%; - - &:not(:last-of-type) { - margin-bottom: 8px; - } -} - -.DTPFilter__objectsCount { - margin-left: 8px; - color: #9baac3; -} - -.DTPFilter__icon { - margin-right: 2px; - vertical-align: text-bottom; -} diff --git a/features/DTP/Filter/DTPFilter.state.ts b/features/DTP/Filter/DTPFilter.state.ts deleted file mode 100644 index 280736ae..00000000 --- a/features/DTP/Filter/DTPFilter.state.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { DtpParticipantType, DtpSeverityType } from '../types'; -import { - DTPParticipantsAction, - DTPParticipantsState, - DTPSeverityAction, - DTPSeverityState, -} from './DTPFilter.types'; - -export function dtpSeverityReducer(state: DTPSeverityState, action: DTPSeverityAction) { - switch (action.type) { - case 'toggle': - return { ...state, [action.severityType]: !state[action.severityType] }; - default: - return state; - } -} - -export const dtpSeverityInitalState = Object.values(DtpSeverityType).reduce((acc, type) => { - acc[type] = true; - - return acc; -}, {} as DTPSeverityState); - -export function dtpParticipantReducer(state: DTPParticipantsState, action: DTPParticipantsAction) { - switch (action.type) { - case 'toggle': - return { ...state, [action.participantType]: !state[action.participantType] }; - default: - return state; - } -} - -export const dtpParticipantInitalState = Object.values(DtpParticipantType).reduce((acc, type) => { - acc[type] = true; - - return acc; -}, {} as DTPParticipantsState); diff --git a/features/DTP/Filter/DTPFilter.tsx b/features/DTP/Filter/DTPFilter.tsx index 93b38138..101e00f7 100644 --- a/features/DTP/Filter/DTPFilter.tsx +++ b/features/DTP/Filter/DTPFilter.tsx @@ -1,83 +1,32 @@ -import React, { useCallback, useEffect, useMemo, useReducer, useState } from 'react'; +import React, { useCallback } from 'react'; import { useDispatch } from 'react-redux'; -import { Checkbox, ListGrid, ListGridItem, MinMax } from 'ekb'; -import { setFilter } from 'state/features/dataLayers'; -import { dtp, DTPFiltersParams } from 'features/DTP/dtp'; -import { FilterLoader } from 'components/Filters/FilterLoader'; +import { MinMax } from 'ekb'; +import { updateFilterParams } from 'state/features/dataLayers'; +import { dtp, DTP_PARTICIPANTS_ITEMS } from 'features/DTP/dtp'; +import { LayerFilter } from 'features/Filters/LayerFilter'; +import { MapFilter } from 'features/Filters/MapFilter'; import { FilterType } from 'types/Filters.types'; import { RangeBaseFilter } from 'components/RangeBaseFilter'; -import { DtpParticipantType, DtpSeverityType } from '../types'; -import { - MIN_DTP_YEAR, - CURRENT_YEAR, - DTP_PARTICIPANT_CONFIG, - SEVERITY_CONFIG, - YEARS_FILTERS_DATA, -} from '../DTP.constants'; -import { - dtpParticipantInitalState, - dtpParticipantReducer, - dtpSeverityInitalState, - dtpSeverityReducer, -} from './DTPFilter.state'; +import { MIN_DTP_YEAR, MAX_DTP_YEAR, YEARS_FILTERS_DATA } from '../DTP.constants'; import styles from './DTPFilter.module.css'; -type ParticipantCountEntries = [DtpParticipantType, number][]; -type SeverityCountEntries = [DtpSeverityType, number][]; - -const DEFAULT_MIN_YEAR = CURRENT_YEAR - 1; - export function DTPFilter() { const dispatch = useDispatch(); - const [severityState, dispatchSeverity] = useReducer( - dtpSeverityReducer, - dtpSeverityInitalState, - ); - const [participantState, dispatchParticipant] = useReducer( - dtpParticipantReducer, - dtpParticipantInitalState, - ); - const [yearsLoaded, setYearsLoaded] = useState(false); - const [participantCount, setParticipantCount] = useState(null); - const [severityCount, setSeverityCount] = useState(null); - const [yearsState, setYearsState] = useState({ - min: DEFAULT_MIN_YEAR, - max: CURRENT_YEAR, - }); - - useEffect(() => { - dtp.getParticipantFilters().then((participantFilters: ParticipantCountEntries) => { - const sortedParticipantCount = participantFilters.sort( - ([, countA], [, countB]) => countB - countA, - ); - - setParticipantCount(sortedParticipantCount); - }); - }, []); - useEffect(() => { - dtp.getSeverityFilters().then((severityFilters: SeverityCountEntries) => { - const sortedSeverityCount = severityFilters.sort( - ([, countA], [, countB]) => countB - countA, + const onYearsChange = useCallback( + (range: MinMax) => { + dispatch( + updateFilterParams({ + activeFilter: FilterType.DTP, + activeFilterParams: { + dtp: { range }, + }, + }), ); - - setSeverityCount(sortedSeverityCount); - }); - }, []); - - const onSeverityChange = useCallback( - (severityType: DtpSeverityType) => dispatchSeverity({ type: 'toggle', severityType }), - [], + }, + [dispatch], ); - const onParticipantChange = useCallback( - (participantType: DtpParticipantType) => - dispatchParticipant({ type: 'toggle', participantType }), - [], - ); - - const onYearsChange = useCallback((range: MinMax) => setYearsState(range), []); - const getHistogramData = useCallback(async () => { const dtpFilters = await dtp.getYearsFilters(); @@ -86,126 +35,30 @@ export function DTPFilter() { value: dtpFilters[idx], })); - setYearsLoaded(true); - return histogramData; }, []); - useEffect(() => { - const filters: DTPFiltersParams = { - years: { - from: yearsState.min, - to: yearsState.max, - }, - }; - - const severities = Object.entries(severityState).reduce((acc, [type, value]) => { - if (value) { - acc.push(type); - } - - return acc; - }, []) as DtpSeverityType[]; - const participants = Object.entries(participantState).reduce((acc, [type, value]) => { - if (value) { - acc.push(type); - } - - return acc; - }, []) as DtpParticipantType[]; - - filters.severity = severities; - filters.participants = participants; - - dispatch( - setFilter({ - activeFilter: FilterType.DTP, - activeFilterParams: filters, - }), - ); - }, [dispatch, severityState, participantState, yearsState.min, yearsState.max]); - - const shouldShowLoader = useMemo( - () => !yearsLoaded || !participantCount || !severityCount, - [yearsLoaded, participantCount, severityCount], - ); - - const shouldShowCheckboxes = useMemo( - () => participantCount && severityCount, - [participantCount, severityCount], - ); - return ( <> - {shouldShowCheckboxes && ( - <> -

    Участники ДТП

    - - {participantCount?.map(([type, count]) => ( - { - onParticipantChange(type as DtpParticipantType); - }} - /> - } - > - {DTP_PARTICIPANT_CONFIG[type]?.label || type} - - ))} - -

    Вред здоровью

    +

    Участники ДТП

    - - {severityCount?.map(([type, count]) => ( - { - onSeverityChange(type as DtpSeverityType); - }} - /> - } - > - {DTP_PARTICIPANT_CONFIG[type]?.label || type} - - ))} - - {/* {severityCount?.map(([type, count], i) => ( - -
    - {type} - {count} -
    -
    - ))} */} - - )} - {shouldShowLoader && } + + +

    Вред здоровью

    + + ); } diff --git a/features/DTP/Filter/DTPFilter.types.ts b/features/DTP/Filter/DTPFilter.types.ts deleted file mode 100644 index 8c594adc..00000000 --- a/features/DTP/Filter/DTPFilter.types.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { DtpParticipantType, DtpSeverityType } from '../types'; - -export interface DTPSeverityAction { - type: 'toggle'; - severityType: DtpSeverityType; -} -export interface DTPParticipantsAction { - type: 'toggle'; - participantType: DtpParticipantType; -} - -export type DTPSeverityState = Record; -export type DTPParticipantsState = Record; diff --git a/features/DTP/dtp.ts b/features/DTP/dtp.ts index 4d3d75f9..c4ec7c73 100644 --- a/features/DTP/dtp.ts +++ b/features/DTP/dtp.ts @@ -1,59 +1,44 @@ -import groupBy from 'lodash/groupBy'; -import { Range } from 'ekb'; import { DTP_YEARS_RANGE } from 'features/DTP/DTP.constants'; import { fetchAPI } from 'shared/helpers/fetchApi'; import dtpData from 'public/ekb-dtp.json'; -import { DtpParticipantType, DtpSeverityType } from './types'; +import { DtpSeverityType } from './types'; -export const dtp = { - async getObject(id: string): Promise { - return fetchAPI(`/api/dtp?id=${id}`); - }, - async getSeverityFilters() { - const dtpBySeverity = Object.entries( - groupBy(dtpData.features, (item) => item.properties.severity), - ) - .map(([type, items]) => [type, items.length]) - .sort((a, b) => (b[1] as number) - (a[1] as number)); +const categories = Array.from( + new Set(dtpData.features.map((item) => item.properties.participant_categories).flat(2)), +); - return Promise.resolve(dtpBySeverity); - }, - async getParticipantFilters() { - const categories = Array.from( - new Set(dtpData.features.map((item) => item.properties.participant_categories).flat(2)), - ); +const dtpByParticipants = categories + .map((category) => [ + category, + dtpData.features.filter((item) => + item.properties.participant_categories.includes(category), + ), + ]) + .map(([type, items]) => [type, items.length]) + .sort((a, b) => (b[1] as number) - (a[1] as number)); - const dtpByParticipants = categories - .map((category) => [ - category, - dtpData.features.filter((item) => - item.properties.participant_categories.includes(category), - ), - ]) - .map(([type, items]) => [type, items.length]) - .sort((a, b) => (b[1] as number) - (a[1] as number)); +export const DTP_PARTICIPANTS_ITEMS = dtpByParticipants.map(([type, count]) => ({ + type: String(type), + subTitle: String(count), +})); - return Promise.resolve(dtpByParticipants); +export const dtp = { + async getObject(id: string): Promise { + return fetchAPI(`/api/dtp?id=${id}`); }, - async getYearsFilters() { + getYearsFilters() { const years = DTP_YEARS_RANGE; const dtpByYear = years.map( (year) => dtpData.features.filter((item) => item.properties.year === year).length, ); - return Promise.resolve(dtpByYear); + return dtpByYear; }, }; -export interface DTPFiltersParams { - severity?: DtpSeverityType[]; - participants?: DtpParticipantType[]; - years: Range; -} - export type DTPObject = { properties: DTPObjectProperties; geometry: { diff --git a/features/DesignCode/CardContent/CardContent.tsx b/features/DesignCode/CardContent/CardContent.tsx deleted file mode 100644 index 7770caff..00000000 --- a/features/DesignCode/CardContent/CardContent.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { Tag } from 'ekb'; -import { Header } from 'components/Card/components/Header/Header'; -import { Section } from 'components/Card/components/Section/Section'; -import { DESIGN_MAP_HOST } from 'features/DesignCode/designCode'; -import styles from 'styles/CardContent.module.css'; -import { Sources } from 'components/Card/components/Sources/Sources'; -import { SOURCES_BY_TYPE } from 'constants/sources'; -import { SourceType } from 'types/Sources.types'; -import { DesignCodeObject } from '../designCodeObject'; -import { DESIGN_CODE_ITEMS_COLORS } from '../DesignCode.constants'; - -export function DesignCodeCardContent({ placemark }: { placemark?: DesignCodeObject }) { - return placemark ? ( -
    -
    - {placemark.street && ( -
    {placemark.street}
    - )} -
    - {placemark.type} -
    -
    - {placemark.images.map((image) => { - const imageData = image.m || image.s; - const imageSrc = `${DESIGN_MAP_HOST}${imageData.src}`; - - return ( - - {placemark.name} - - ); - })} -
    -
    - -
    -
    - ) : null; -} diff --git a/features/DesignCode/DesignCode.constants.ts b/features/DesignCode/DesignCode.constants.ts index 9dc9a1a4..7dccbaa0 100644 --- a/features/DesignCode/DesignCode.constants.ts +++ b/features/DesignCode/DesignCode.constants.ts @@ -1,17 +1,15 @@ -import { DesignCodeItemType } from './designCodeObject'; - export const DESIGN_CODE_ITEMS_COLORS = { - [DesignCodeItemType.AddressPlate]: '#ff640a', - [DesignCodeItemType.ChoPlate]: '#e63223', - [DesignCodeItemType.CommemorativePlaque]: '#f758b6', - [DesignCodeItemType.HistoricAddressPlate]: '#aa9b46', - [DesignCodeItemType.LogosAndIdentic]: '#00b400', - [DesignCodeItemType.NavigationStela]: '#ffd400', - [DesignCodeItemType.OKN]: '#00b4ff', - [DesignCodeItemType.StopFreeze]: '#55647d', - [DesignCodeItemType.StreetFurniture]: '#5820e4', - [DesignCodeItemType.TrafficLight]: '#965a14', - [DesignCodeItemType.Transport]: '#006d4e', - [DesignCodeItemType.WallPlate]: '#a00041', - [DesignCodeItemType.ColumnsWithArrows]: '#86e621', + 'Обычные адресные таблички': '#ff640a', + 'Таблички ЧО': '#e63223', + 'Памятные таблички': '#f758b6', + 'Исторические адресные таблички': '#aa9b46', + 'Логотипы и айдентика': '#00b400', + 'Навигационные стелы': '#ffd400', + 'Таблички ОКН': '#00b4ff', + 'Фризы остановок': '#55647d', + 'Уличная мебель': '#5820e4', + Светофор: '#965a14', + Транспорт: '#006d4e', + 'Настенные таблички': '#a00041', + 'Столбы со стрелками': '#86e621', }; diff --git a/features/DesignCode/DesignCodeCard.tsx b/features/DesignCode/DesignCodeCard.tsx new file mode 100644 index 00000000..00635069 --- /dev/null +++ b/features/DesignCode/DesignCodeCard.tsx @@ -0,0 +1,67 @@ +import { Card, Tag } from 'ekb'; +import { DESIGN_MAP_HOST } from 'features/DesignCode/designCode'; +import { CardActions } from 'components/Card/components/CardActions'; +import { Sources } from 'components/Card/components/Sources/Sources'; +import { SOURCES_BY_TYPE } from 'constants/sources'; +import { SourceType } from 'types/Sources.types'; +import styles from 'styles/CardContent.module.css'; +import { DesignCodeObject } from './designCodeObject'; +import { DESIGN_CODE_ITEMS_COLORS } from './DesignCode.constants'; + +export function DesignCodeCardContent({ placemark }: { placemark?: DesignCodeObject }) { + if (!placemark) { + return null; + } + + return ( + } + title={placemark.name} + description={placemark.description} + additionalInfo={[placemark.street].filter(Boolean)} + blocks={[ + { + type: 'value', + value: ( + {placemark.type} + ), + }, + { type: 'divider' }, + { + type: 'section', + value: ( + <> + {placemark.images.map((image) => { + const imageData = image.m || image.s; + const imageSrc = `${DESIGN_MAP_HOST}${imageData.src}`; + + return ( + + {placemark.name} + + ); + })} + + ), + }, + { type: 'divider' }, + { + type: 'section', + title: 'Источники', + value: , + }, + ]} + /> + ); +} diff --git a/features/DesignCode/DesignCodeSource.tsx b/features/DesignCode/DesignCodeSource.tsx deleted file mode 100644 index f8afab69..00000000 --- a/features/DesignCode/DesignCodeSource.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import React, { useEffect } from 'react'; -import { useMap, Source, Layer, Marker, CircleLayer } from 'react-map-gl'; -import { useSelector } from 'react-redux'; -import classNames from 'classnames'; -import { activeFilterSelector, activeFilterParamsSelector } from 'state/features/selectors'; -import { FilterType } from 'types/Filters.types'; -import { DESIGN_CODE_ITEMS_COLORS } from 'features/DesignCode/DesignCode.constants'; -import { MapItemType } from 'types/Content.types'; -import { DESIGN_MAP_HOST } from 'features/DesignCode/designCode'; -import { useOpenMapItem } from 'features/Map/helpers/useOpenMapItem'; -import { getLayerStyle } from 'features/Map/helpers/getFeatureState'; -import geojson from 'public/ekb-design-code.json'; -import { usePopup } from 'features/Map/providers/usePopup'; -import useMapObjectState from 'features/Map/helpers/useMapObjectState'; -import styles from './DesignCodeMarker.module.css'; - -const DESIGN_CODE_LAYER_ID = 'design-code-point'; - -const DESIGN_CODE_MARKER_CLICKABLE_SIZE = 22; -const DESIGN_CODE_MARKER_IMAGE_SIZE = 40; - -export function DesignCodeSource() { - const { popupId } = usePopup(); - const activeFilter = useSelector(activeFilterSelector); - const activeFilterParams = useSelector(activeFilterParamsSelector); - - useMapObjectState(DESIGN_CODE_LAYER_ID); - useOpenMapItem(DESIGN_CODE_LAYER_ID, MapItemType.DesignCode); - - if (activeFilter !== FilterType.DesignCode || !activeFilterParams) { - return null; - } - - const items = geojson.features.filter((item) => activeFilterParams[item.properties.type]); - - const fakeClickableMarkersLayerStyle: CircleLayer = { - id: DESIGN_CODE_LAYER_ID, - type: 'circle', - source: 'ekb-design-code-source', - paint: { - 'circle-radius': getLayerStyle({ - initial: DESIGN_CODE_MARKER_CLICKABLE_SIZE, - hover: DESIGN_CODE_MARKER_CLICKABLE_SIZE * 1.2, - active: DESIGN_CODE_MARKER_CLICKABLE_SIZE * 1.3, - }), - 'circle-opacity': 1, - }, - }; - - return ( - - - {items.map((feature) => ( - - {feature.properties.description} - - ))} - - ); -} diff --git a/features/DesignCode/Filter/DesignCodeFilter.state.ts b/features/DesignCode/Filter/DesignCodeFilter.state.ts deleted file mode 100644 index 55063377..00000000 --- a/features/DesignCode/Filter/DesignCodeFilter.state.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { DesignCodeItemType } from '../designCodeObject'; -import { DesignCodeFilterState, DesignCodeFilterAction } from './DesignCodeFilter.types'; - -export function designCodeReducer(state: DesignCodeFilterState, action: DesignCodeFilterAction) { - switch (action.type) { - case 'toggle': - return { ...state, [action.designCodeItemType]: !state[action.designCodeItemType] }; - default: - return state; - } -} - -export const designCondeInitalState = Object.values(DesignCodeItemType).reduce((acc, type) => { - acc[type] = true; - - return acc; -}, {} as DesignCodeFilterState); diff --git a/features/DesignCode/Filter/DesignCodeFilter.tsx b/features/DesignCode/Filter/DesignCodeFilter.tsx deleted file mode 100644 index cd1edc44..00000000 --- a/features/DesignCode/Filter/DesignCodeFilter.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import React, { useCallback, useEffect, useReducer } from 'react'; -import { useDispatch } from 'react-redux'; -import { Checkbox, ListGrid, ListGridItem } from 'ekb'; -import groupBy from 'lodash/groupBy'; -import { FilterType } from 'types/Filters.types'; -import { setFilter } from 'state/features/dataLayers'; -import designCode from 'public/ekb-design-code.json'; -import { DESIGN_CODE_ITEMS_COLORS } from '../DesignCode.constants'; -import { DesignCodeItemType } from '../designCodeObject'; -import { designCodeReducer, designCondeInitalState } from './DesignCodeFilter.state'; - -const DESIGN_CODE_ITEMS = groupBy(designCode.features, (item) => item.properties.type); - -export function DesignCodeFilter() { - const dispatch = useDispatch(); - const [designCodeFilterState, dispatchDesignCodeAction] = useReducer( - designCodeReducer, - designCondeInitalState, - ); - - const onChange = useCallback( - (designCodeItemType: DesignCodeItemType) => - dispatchDesignCodeAction({ type: 'toggle', designCodeItemType }), - [], - ); - - useEffect(() => { - dispatch( - setFilter({ - activeFilter: FilterType.DesignCode, - activeFilterParams: designCodeFilterState, - }), - ); - }, [designCodeFilterState, dispatch]); - - return ( - - {Object.entries(DESIGN_CODE_ITEMS).map(([type, items]) => ( - onChange(type as DesignCodeItemType)} - /> - } - > - {type} - - ))} - - ); -} diff --git a/features/DesignCode/Filter/DesignCodeFilter.types.ts b/features/DesignCode/Filter/DesignCodeFilter.types.ts deleted file mode 100644 index 2c0d0807..00000000 --- a/features/DesignCode/Filter/DesignCodeFilter.types.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { DesignCodeItemType } from '../designCodeObject'; - -export type DesignCodeFilterState = Record; -export interface DesignCodeFilterAction { - type: 'toggle'; - designCodeItemType: DesignCodeItemType; -} diff --git a/features/DesignCode/designCode.ts b/features/DesignCode/designCode.ts index ab00bd47..afcc3b75 100644 --- a/features/DesignCode/designCode.ts +++ b/features/DesignCode/designCode.ts @@ -1,23 +1,20 @@ -import groupBy from 'lodash/groupBy'; -import dtp from 'public/ekb-design-code.json'; -import { DesignCodeItemType, DesignCodeObject } from './designCodeObject'; +import { FeatureCollection } from 'geojson'; +import dc from 'public/ekb-design-code.json'; +import { groupByProperty } from 'features/Map/helpers/groupByProperty'; +import { DesignCodeObject } from './designCodeObject'; export const DESIGN_MAP_HOST = 'https://map.ekaterinburg.design'; -const designByType = Object.entries(groupBy(dtp.features, (item) => item.properties.type)) - .map(([type, items]) => [type, items.length]) - .sort((a, b) => (b[1] as number) - (a[1] as number)); +export const DESIGN_CODE_ITEMS = groupByProperty(dc as FeatureCollection); export const designCode = { getObject(id: string): Promise { try { - const result = dtp.features.find((item) => item.properties.id === id); + const result = dc.features.find((item) => item.properties.id === id); // @ts-ignore return Promise.resolve({ ...result.properties, - street: result.properties.street, - type: result.properties.type as DesignCodeItemType, coords: [result.geometry.coordinates[0], result.geometry.coordinates[1]], }); } catch (error) { @@ -25,7 +22,4 @@ export const designCode = { return Promise.resolve(null); } }, - async getObjectsCount() { - return designByType; - }, }; diff --git a/features/Facade/CardContent/Facade.module.css b/features/Facade/CardContent/Facade.module.css deleted file mode 100644 index c8b0c5b1..00000000 --- a/features/Facade/CardContent/Facade.module.css +++ /dev/null @@ -1,12 +0,0 @@ -.facade { - display: flex; - gap: 8px; - flex-direction: column; -} - -.facade_title { - font-size: 16px; - line-height: 1.3; - font-weight: 400; - margin: 0; -} diff --git a/features/Facade/CardContent/Facade.tsx b/features/Facade/CardContent/Facade.tsx deleted file mode 100644 index 4c29234e..00000000 --- a/features/Facade/CardContent/Facade.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { DownloadButton } from 'shared/UI/DownloadButton/DownloadButton'; -import styles from './Facade.module.css'; - -type Props = { - facade: { - name: string; - link: string; - }; -}; - -export default function Facade({ facade }: Props) { - return ( -
    -

    Дизайн-код фасада

    - -
    - ); -} diff --git a/shared/UI/DownloadButton/DownloadButton.module.css b/features/Facade/DownloadButton/DownloadButton.module.css similarity index 100% rename from shared/UI/DownloadButton/DownloadButton.module.css rename to features/Facade/DownloadButton/DownloadButton.module.css diff --git a/shared/UI/DownloadButton/DownloadButton.tsx b/features/Facade/DownloadButton/DownloadButton.tsx similarity index 84% rename from shared/UI/DownloadButton/DownloadButton.tsx rename to features/Facade/DownloadButton/DownloadButton.tsx index a2f18080..59fffc81 100644 --- a/shared/UI/DownloadButton/DownloadButton.tsx +++ b/features/Facade/DownloadButton/DownloadButton.tsx @@ -1,5 +1,5 @@ import classNames from 'classnames'; -import DownloadIcon from 'shared/UI/Icons/DownloadIcon'; +import { Icon, IconType } from 'ekb'; import styles from './DownloadButton.module.css'; type Props = { @@ -22,7 +22,7 @@ export function DownloadButton({ type, name, link }: Props) { rel="noreferrer" >
    - +

    {name}

    diff --git a/features/Facade/Filter/FacadeFilter.tsx b/features/Facade/FacadeFilter.tsx similarity index 100% rename from features/Facade/Filter/FacadeFilter.tsx rename to features/Facade/FacadeFilter.tsx diff --git a/features/FeedbackButton/FeedbackButton.tsx b/features/FeedbackButton/FeedbackButton.tsx index 750d4eb8..3e826149 100644 --- a/features/FeedbackButton/FeedbackButton.tsx +++ b/features/FeedbackButton/FeedbackButton.tsx @@ -1,6 +1,4 @@ -import { Button, ButtonSize, ButtonType } from 'ekb'; -import { Icon } from 'shared/UI/Icons'; -import { IconType } from 'shared/UI/Icons/Icons.types'; +import { Button, ButtonSize, ButtonType, Icon, IconType } from 'ekb'; const getEditObjectLink = (address: string) => `https://tally.so#tally-open=w2BoVe&tally-width=650&tally-overlay=1&tally-emoji-animation=none&address=${address}`; @@ -18,7 +16,7 @@ export function FeedbackButton({ address }: Props) { type={ButtonType.DEFAULT} size={ButtonSize.MEDIUM} href={href} - prefix={} + prefix={} > Дополнить или поправить diff --git a/components/Filters/Filters.tsx b/features/Filters/Filters.tsx similarity index 54% rename from components/Filters/Filters.tsx rename to features/Filters/Filters.tsx index 854f1374..98cc92a5 100644 --- a/components/Filters/Filters.tsx +++ b/features/Filters/Filters.tsx @@ -1,6 +1,7 @@ import React from 'react'; import styled from 'styled-components'; import { Accordion, AccordionItem, Link, LinkSize } from 'ekb'; +import { MapFilter } from 'features/Filters/MapFilter'; import { FilterConfigItem, FilterType } from 'types/Filters.types'; interface Props { @@ -36,24 +37,23 @@ export function Filters({ filters, activeFilter, onToggleClick }: Props) { horizontalGap={16} verticalGap={16} > - {component && ( - <> - {isActive ? component : null} - {!isVerified && ( - - Данные берутся из публичных источников - и содержат неточности.{' '} - - Оставьте фидбек - -  — помогите улучшить карту. - - )} - - )} + <> + {isActive && component ? component : null} + {isActive ? : null} + {!isVerified && component && ( + + Данные берутся из публичных источников и содержат + неточности.{' '} + + Оставьте фидбек + +  — помогите улучшить карту. + + )} + ); }, diff --git a/features/Filters/LayerFilter.tsx b/features/Filters/LayerFilter.tsx new file mode 100644 index 00000000..bca45920 --- /dev/null +++ b/features/Filters/LayerFilter.tsx @@ -0,0 +1,38 @@ +import React, { useCallback } from 'react'; +import { useDispatch } from 'react-redux'; +import { FilterGrid, IFilterGridItem } from 'components/Filters/FilterGrid'; +import { updateFilterParams } from 'state/features/dataLayers'; + +interface Props { + filterType: string; + items?: IFilterGridItem[]; + selectedByDefault?: string[]; + filterItemKey?: string; +} + +export function LayerFilter({ + filterType, + items, + selectedByDefault = items.map((item) => item.type), + filterItemKey, +}: Props) { + const dispatch = useDispatch(); + + const onChange = useCallback( + (selected) => { + dispatch( + updateFilterParams({ + activeFilter: filterType, + activeFilterParams: { + [filterType]: { + [filterItemKey]: selected, + }, + }, + }), + ); + }, + [dispatch, filterItemKey, filterType], + ); + + return ; +} diff --git a/features/Filters/MapFilter.tsx b/features/Filters/MapFilter.tsx new file mode 100644 index 00000000..95ffd9b6 --- /dev/null +++ b/features/Filters/MapFilter.tsx @@ -0,0 +1,52 @@ +import React, { useEffect, useState } from 'react'; +import { FeatureCollection } from 'geojson'; +import { LayerFilter } from 'features/Filters/LayerFilter'; +import { state } from 'state/config'; +import { groupByProperty } from 'features/Map/helpers/groupByProperty'; +import { FilterLoader } from 'components/Filters/FilterLoader'; +import { fetchAPI } from 'shared/helpers/fetchApi'; + +export function MapFilter({ + filterType, + filterItemKey = filterType, +}: { + filterType: string; + filterItemKey?: string; +}) { + const [data, setData] = useState(null); + const filter = state.filters[filterItemKey]; + + useEffect(() => { + if (filter?.type) { + fetchAPI(filter.path).then(setData); + } + }, [filter?.path, filter?.type]); + + if (!filter?.type) { + return null; + } + + if (!data) { + return ; + } + + switch (filter.type) { + case 'checkboxes': { + const items = groupByProperty(data as FeatureCollection, filter.property).map( + (item) => ({ + type: item.type, + subTitle: item.count, + color: filter.values[item.type]?.color, + description: filter.values[item.type]?.description, + }), + ); + + return ( + + ); + } + + default: + return null; + } +} diff --git a/features/HouseAge/HouseAgeFilter.tsx b/features/HouseAge/HouseAgeFilter.tsx index 86bd137d..5d9422ba 100644 --- a/features/HouseAge/HouseAgeFilter.tsx +++ b/features/HouseAge/HouseAgeFilter.tsx @@ -4,12 +4,10 @@ import { MinMax } from 'ekb'; import { houseBase } from 'features/Buildings/houseBase'; import { AGE_FILTERS_DATA, HouseSourceType } from 'features/Buildings/Houses.constants'; import { setFilterParams } from 'state/features/dataLayers'; -import { HouseBaseFilter } from '../Buildings/Filter/HouseBaseFilter'; +import { HouseBaseFilter } from '../Buildings/HouseBaseFilter'; export const EKATERINBURG_FOUNDATION_YEAR = 1723; -export const CURRENT_YEAR = new Date().getFullYear(); - export function HouseAgeFilter() { const dispatch = useDispatch(); @@ -34,7 +32,7 @@ export function HouseAgeFilter() { return ( diff --git a/features/HouseFloor/HouseFloorFilter.tsx b/features/HouseFloor/HouseFloorFilter.tsx index 521979cd..1182067e 100644 --- a/features/HouseFloor/HouseFloorFilter.tsx +++ b/features/HouseFloor/HouseFloorFilter.tsx @@ -5,7 +5,7 @@ import { houseBase } from 'features/Buildings/houseBase'; import { setFilterParams } from 'state/features/dataLayers'; import { FLOOR_FILTERS_DATA, HouseSourceType } from 'features/Buildings/Houses.constants'; -import { HouseBaseFilter } from '../Buildings/Filter/HouseBaseFilter'; +import { HouseBaseFilter } from '../Buildings/HouseBaseFilter'; export const MIN_FLOOR = 1; export const MAX_FLOOR = 52; diff --git a/features/HouseWearTear/HealthProgress/HealthProgress.tsx b/features/HouseWearTear/HealthProgress/HealthProgress.tsx index a0891058..83e91d9a 100644 --- a/features/HouseWearTear/HealthProgress/HealthProgress.tsx +++ b/features/HouseWearTear/HealthProgress/HealthProgress.tsx @@ -1,6 +1,5 @@ import { useMemo } from 'react'; -import BrokenHeart from 'shared/UI/Icons/BrokenHeart'; -import Heart from 'shared/UI/Icons/Heart'; +import { Icon, IconType } from 'ekb'; import { WEAR_TEAR_FILTERS_DATA } from 'features/Buildings/Houses.constants'; import styles from './HealthProgress.module.css'; @@ -26,9 +25,9 @@ function HealthProgress({ percent, isEmergency }: Props) { >
    {isEmergency ? ( - + ) : ( - + )}
    diff --git a/features/HouseWearTear/HouseWearTearFilter.tsx b/features/HouseWearTear/HouseWearTearFilter.tsx index 6895eca2..8e6ed881 100644 --- a/features/HouseWearTear/HouseWearTearFilter.tsx +++ b/features/HouseWearTear/HouseWearTearFilter.tsx @@ -1,7 +1,7 @@ import React, { useCallback } from 'react'; import { useDispatch } from 'react-redux'; import { MinMax } from 'ekb'; -import { HouseBaseFilter } from 'features/Buildings/Filter/HouseBaseFilter'; +import { HouseBaseFilter } from 'features/Buildings/HouseBaseFilter'; import { houseBase } from 'features/Buildings/houseBase'; import { HouseSourceType, WEAR_TEAR_FILTERS_DATA } from 'features/Buildings/Houses.constants'; import { setFilterParams } from 'state/features/dataLayers'; diff --git a/features/Lines/CardContent/CardContent.tsx b/features/Lines/CardContent/CardContent.tsx deleted file mode 100644 index 96e4e548..00000000 --- a/features/Lines/CardContent/CardContent.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import React from 'react'; -import { Link, LinkSize } from 'ekb'; -import { Header } from 'components/Card/components/Header/Header'; -import { Section } from 'components/Card/components/Section/Section'; -import styles from 'styles/CardContent.module.css'; -import { LineObject } from '../lineType'; - -type TLinesCardContentProps = { - placemark?: LineObject; -}; - -export function LinesCardContent({ placemark }: TLinesCardContentProps) { - if (!placemark) return null; - - return ( -
    -
    - {placemark.properties.description?.startsWith('http') && ( -
    - - Подробнее об объекте - -
    - )} -
    - ); -} diff --git a/features/Lines/Filter/LinesFilter.state.ts b/features/Lines/Filter/LinesFilter.state.ts deleted file mode 100644 index 0508d6ad..00000000 --- a/features/Lines/Filter/LinesFilter.state.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { LineType } from '../lineType'; -import { LinesState, LinesAction } from './LinesFilter.types'; - -export function linesReducer(state: LinesState, action: LinesAction) { - switch (action.type) { - case 'toggle': - return { ...state, [action.lineType]: !state[action.lineType] }; - default: - return state; - } -} - -export const linesInitalState = Object.values(LineType).reduce((acc, type) => { - acc[type] = true; - - return acc; -}, {} as LinesState); diff --git a/features/Lines/Filter/LinesFilter.tsx b/features/Lines/Filter/LinesFilter.tsx deleted file mode 100644 index 47a780e6..00000000 --- a/features/Lines/Filter/LinesFilter.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import React, { useCallback, useEffect, useReducer, useState } from 'react'; -import { useDispatch } from 'react-redux'; -import { Checkbox, ListGrid, ListGridItem } from 'ekb'; -import { setFilter } from 'state/features/dataLayers'; -import { FilterType } from 'types/Filters.types'; -import { lines } from 'features/Lines/lines'; -import { FilterLoader } from 'components/Filters/FilterLoader'; -import { LineType } from '../lineType'; -import { LINES_CONFIG } from '../Lines.constants'; -import { linesInitalState, linesReducer } from './LinesFilter.state'; - -type LinesCountEntries = [LineType, number][]; - -export function LinesFilter() { - const dispatch = useDispatch(); - const [linesState, dispatchLines] = useReducer(linesReducer, linesInitalState); - const [linesCount, setLinesCount] = useState(null); - - useEffect(() => { - lines.getFilters().then((linesFilters: LinesCountEntries) => { - const sortedLinesCount = linesFilters.sort(([, countA], [, countB]) => countB - countA); - - setLinesCount(sortedLinesCount); - }); - }, []); - - useEffect(() => { - dispatch( - setFilter({ - activeFilter: FilterType.Line, - activeFilterParams: linesState, - }), - ); - }, [dispatch, linesState]); - - const onLinesChange = useCallback( - (lineType: LineType) => dispatchLines({ type: 'toggle', lineType }), - [], - ); - - if (!linesCount) return ; - - return ( - - {linesCount.map(([type, count]) => ( - onLinesChange(type as LineType)} - /> - } - > - {type} - - ))} - - ); -} diff --git a/features/Lines/Filter/LinesFilter.types.ts b/features/Lines/Filter/LinesFilter.types.ts deleted file mode 100644 index 54877233..00000000 --- a/features/Lines/Filter/LinesFilter.types.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { LineType } from '../lineType'; - -export interface LinesAction { - type: 'toggle'; - lineType: LineType; -} - -export type LinesState = Record; diff --git a/features/Lines/Lines.constants.ts b/features/Lines/Lines.constants.ts deleted file mode 100644 index 82e96aa2..00000000 --- a/features/Lines/Lines.constants.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { LineType } from './lineType'; - -export const LINES_CONFIG = { - [LineType.RedLine]: { - color: '#e31e24', - description: 'Маршрут по\u00A0историческому центру города', - }, - [LineType.BlueLine]: { - color: '#189eda', - description: 'Маршрут по\u00A0местам, связанным с\u00A0царской семьей', - }, - [LineType.PurpleLine]: { - color: '#9747ff', - description: 'Арт-объекты фестиваля уличного искусства «Стенограффия»', - }, -}; diff --git a/features/Lines/LinesCardContent.tsx b/features/Lines/LinesCardContent.tsx new file mode 100644 index 00000000..5c28e2d6 --- /dev/null +++ b/features/Lines/LinesCardContent.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { Card, Link, LinkSize } from 'ekb'; +import { CardActions } from 'components/Card/components/CardActions'; + +export interface LineObject { + properties: { + id: number; + title?: string; + description: null | string; + }; + geometry: { + id: string; + coordinates: [lat: number, lng: number]; + type?: string; + }; +} + +type TLinesCardContentProps = { + placemark?: LineObject; +}; + +export function LinesCardContent({ placemark }: TLinesCardContentProps) { + if (!placemark) return null; + + return ( + } + title={placemark.properties.title || placemark.properties.description} + blocks={[ + ...(placemark.properties.description?.startsWith('http') + ? [ + { + type: 'value' as const, + value: ( + + Подробнее об объекте + + ), + }, + ] + : undefined), + ]} + /> + ); +} diff --git a/features/Lines/LinesSource.tsx b/features/Lines/LinesSource.tsx deleted file mode 100644 index f82e8047..00000000 --- a/features/Lines/LinesSource.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import React from 'react'; -import { Source, Layer } from 'react-map-gl'; -import type { CircleLayer, LineLayer } from 'react-map-gl'; -import { useSelector } from 'react-redux'; -import { activeFilterSelector, activeFilterParamsSelector } from 'state/features/selectors'; -import { FilterType } from 'types/Filters.types'; -import { LINES_CONFIG } from 'features/Lines/Lines.constants'; -import { getLayerStyle } from 'features/Map/helpers/getFeatureState'; -import useMapObjectState from 'features/Map/helpers/useMapObjectState'; -import { useOpenMapItem } from 'features/Map/helpers/useOpenMapItem'; -import { MapItemType } from 'types/Content.types'; - -const LINE_POINTS_LAYER_ID = 'ekb-points-layer'; - -export function LinesSource() { - const activeFilter = useSelector(activeFilterSelector); - const activeFilterParams = useSelector(activeFilterParamsSelector); - - useMapObjectState(LINE_POINTS_LAYER_ID); - - useOpenMapItem(LINE_POINTS_LAYER_ID, MapItemType.LinePoint); - - if (activeFilter !== FilterType.Line || !activeFilterParams) { - return null; - } - - const activeItems = Object.entries(activeFilterParams) - .filter(([, value]) => value) - .map(([type]) => [type, LINES_CONFIG[type]]); - - const colors = activeItems.map(([type, { color }]) => [['==', ['get', 'type'], type], color]); - - const strokeColors = activeItems.map(([type]) => [['==', ['get', 'type'], type], '#000']); - - const pointLayerStyle: CircleLayer = { - id: LINE_POINTS_LAYER_ID, - type: 'circle', - source: 'ekb-points-source', - paint: { - 'circle-radius': getLayerStyle({ initial: 8, hover: 10, active: 12 }), - // @ts-ignore - 'circle-color': ['case'].concat(...colors).concat(['rgba(0, 0, 0, 0)']), - 'circle-stroke-width': 1, - // @ts-ignore - 'circle-stroke-color': ['case'].concat(...strokeColors).concat(['rgba(0, 0, 0, 0)']), - }, - }; - - const linesLayerStyle: LineLayer = { - id: 'lines', - type: 'line', - source: 'ekb-lines-source', - paint: { - // @ts-ignore - 'line-color': ['case'].concat(...colors).concat(['rgba(0, 0, 0, 0)']), - 'line-width': 3, - }, - }; - - return ( - <> - - - - - - - - ); -} diff --git a/features/Lines/lineType.ts b/features/Lines/lineType.ts deleted file mode 100644 index eb5d96ad..00000000 --- a/features/Lines/lineType.ts +++ /dev/null @@ -1,18 +0,0 @@ -export enum LineType { - RedLine = 'Красная линия', - BlueLine = 'Синяя линия', - PurpleLine = 'Фиолетовая линия', -} - -export interface LineObject { - properties: { - id: number; - title?: string; - description: null | string; - }; - geometry: { - id: string; - coordinates: [lat: number, lng: number]; - type?: string; - }; -} diff --git a/features/Lines/lines.ts b/features/Lines/lines.ts index 4e97720c..7e0890d3 100644 --- a/features/Lines/lines.ts +++ b/features/Lines/lines.ts @@ -1,16 +1,10 @@ -import groupBy from 'lodash/groupBy'; import data from 'public/ekb-color-points.json'; -const linesByType = Object.entries(groupBy(data.features, (item) => item.properties.type)) - .map(([type, items]) => [type, items.length]) - .sort((a, b) => (b[1] as number) - (a[1] as number)); - export const lines = { + async getAll(): Promise { + return data; + }, async getObject(id: string): Promise { return data.features.find((f) => String(f.properties.id) === id); }, - - async getFilters() { - return Promise.resolve(linesByType); - }, }; diff --git a/features/Map/ClickableLayer.tsx b/features/Map/ClickableLayer.tsx new file mode 100644 index 00000000..eef5c079 --- /dev/null +++ b/features/Map/ClickableLayer.tsx @@ -0,0 +1,27 @@ +import { Layer } from 'react-map-gl'; +import { IVisualisationLayer } from 'state/config'; +import { MapItemType } from 'types/Content.types'; +import { getLayerProps } from './helpers/getLayerProps'; +import useMapObjectState from './helpers/useMapObjectState'; +import { useOpenMapItem } from './helpers/useOpenMapItem'; +import { MarkersLayer } from './MarkersLayer'; + +function ClickableLayer({ id }: { id: string }) { + useMapObjectState(id); + useOpenMapItem(id, id as MapItemType); + + return null; +} + +export function MapLayer(layerProps: IVisualisationLayer) { + return ( + <> + {layerProps.openable && } + {layerProps.type === 'marker-image' ? ( + + ) : ( + + )} + + ); +} diff --git a/features/Map/Map.tsx b/features/Map/Map.tsx index 965c0de3..a4c05865 100644 --- a/features/Map/Map.tsx +++ b/features/Map/Map.tsx @@ -7,15 +7,16 @@ import MapGl from 'react-map-gl'; import { MAX_ZOOM, MIN_ZOOM, CENTER_COORDS } from 'constants/map'; import { BuildingSource } from 'features/Buildings/BuildingSource'; -import { DesignCodeSource } from 'features/DesignCode/DesignCodeSource'; import { DtpSource } from 'features/DTP/DtpSource'; -import { LinesSource } from 'features/Lines/LinesSource'; import { OknSource } from 'features/OKN/OknSource'; -import { QuarterSource } from 'features/Quarter/QuarterSource'; import { FacadeSource } from 'features/Facade/FacadeSource'; -import { MapContext } from './providers/MapProvider'; import 'maplibre-gl/dist/maplibre-gl.css'; +import { MapSource } from 'features/Map/MapSource'; +import { state } from 'state/config'; +import { FilterType } from 'types/Filters.types'; +import { MapItemType } from 'types/Content.types'; +import { MapContext } from './providers/MapProvider'; function MapLayers() { return ( @@ -23,9 +24,12 @@ function MapLayers() { - - - + + + ); diff --git a/features/Map/MapSource.tsx b/features/Map/MapSource.tsx new file mode 100644 index 00000000..ee8c5c14 --- /dev/null +++ b/features/Map/MapSource.tsx @@ -0,0 +1,64 @@ +import React, { useEffect } from 'react'; +import { Source, useMap } from 'react-map-gl'; +import { useSelector } from 'react-redux'; +import { activeFilterParamsSelector, activeFilterSelector } from 'state/features/selectors'; +import { FilterType } from 'types/Filters.types'; +import { ILayer, state } from 'state/config'; +import { MapLayer } from 'features/Map/ClickableLayer'; + +export function MapSource({ layer, filterType }: { layer: ILayer; filterType: FilterType }) { + const activeFilter = useSelector(activeFilterSelector); + const activeFilterParams = useSelector(activeFilterParamsSelector); + + const { ekbMap } = useMap(); + + useEffect(() => { + const map = ekbMap?.getMap?.(); + + if (activeFilter !== filterType) { + return; + } + + if (map) { + layer.filters?.forEach((filterId) => { + const filter = state.filters[filterId]; + filter.filterVisualisationLayers.forEach((vlId) => { + if (!map.getLayer(vlId)) { + return; + } + + const v = state.visualisationLayers[vlId]; + const values = activeFilterParams?.[filterType]?.[filterId]; + if (values) { + map.setFilter(v.id, [ + 'in', + ['get', filter.property], + ['literal', activeFilterParams?.[filterType]?.[filterId]], + ]); + } + }); + }); + } + }, [activeFilter, activeFilterParams, ekbMap, filterType, layer, layer.filters, layer.id]); + + if (activeFilter !== filterType) { + return null; + } + + return ( + <> + {layer.visualisationLayers.map((visualId) => ( + + + + ))} + + ); +} diff --git a/features/DesignCode/DesignCodeMarker.module.css b/features/Map/MarkersLayer.module.css similarity index 100% rename from features/DesignCode/DesignCodeMarker.module.css rename to features/Map/MarkersLayer.module.css diff --git a/features/Map/MarkersLayer.tsx b/features/Map/MarkersLayer.tsx new file mode 100644 index 00000000..e4edbc37 --- /dev/null +++ b/features/Map/MarkersLayer.tsx @@ -0,0 +1,84 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { Layer, Marker, CircleLayer } from 'react-map-gl'; +import { useSelector } from 'react-redux'; +import classNames from 'classnames'; +import { FeatureCollection } from 'geojson'; +import { DESIGN_CODE_ITEMS_COLORS } from 'features/DesignCode/DesignCode.constants'; +import { DESIGN_MAP_HOST } from 'features/DesignCode/designCode'; +import { getLayerStyle } from 'features/Map/helpers/getFeatureState'; +import { activeFilterParamsSelector, activeFilterSelector } from 'state/features/selectors'; +import { FilterLoader } from 'components/Filters/FilterLoader'; +import { fetchAPI } from 'shared/helpers/fetchApi'; +import { IVisualisationLayer, state } from 'state/config'; +import { usePopup } from './providers/usePopup'; +import styles from './MarkersLayer.module.css'; + +export function MarkersLayer({ layer }: { layer: IVisualisationLayer }) { + const { popupId } = usePopup(); + const [items, setData] = useState>(null); + const activeFilter = useSelector(activeFilterSelector); + const activeFilterParams = useSelector(activeFilterParamsSelector); + + const markers = useMemo(() => { + const filter = state.filters?.[activeFilter]; + const values = activeFilterParams?.[activeFilter]?.[activeFilter]; + + if (filter && values) { + return items?.features.filter((f) => values.includes(f.properties[filter.property])); + } + + return items?.features; + }, [activeFilter, activeFilterParams, items?.features]); + + useEffect(() => { + if (layer?.type) { + fetchAPI(layer.path).then(setData); + } + }, [layer?.path, layer?.type]); + + if (!layer?.type) { + return null; + } + + if (!items) { + return ; + } + + const fakeClickableMarkersLayerStyle: CircleLayer = { + id: layer.id, + source: layer.source, + type: 'circle', + paint: { + 'circle-opacity': 0, + 'circle-radius': getLayerStyle({ + initial: 22, + hover: 22 * 1.2, + active: 22 * 1.3, + }), + }, + }; + + return ( + <> + + {markers.map((feature) => ( + + {feature.properties.description} + + ))} + + ); +} diff --git a/features/Map/helpers/getLayerProps.ts b/features/Map/helpers/getLayerProps.ts new file mode 100644 index 00000000..2eaeb22a --- /dev/null +++ b/features/Map/helpers/getLayerProps.ts @@ -0,0 +1,47 @@ +import type { CircleLayer, FillLayer, LineLayer } from 'react-map-gl'; +import { IVisualisationLayer } from 'state/config'; + +export function getLayerProps(layer: IVisualisationLayer): CircleLayer | LineLayer | FillLayer { + const colors = Object.entries(layer.values).map(([value, { color }]) => [ + ['==', ['get', 'type'], value], + color, + ]); + + const props = { + id: layer.id, + type: layer.type, + source: layer.source, + }; + + switch (layer.type) { + case 'circle': + return { + ...props, + paint: { + // @ts-ignore + 'circle-color': ['case'].concat(...colors).concat(['rgba(0, 0, 0, 0)']), + 'circle-stroke-color': '#000', + ...layer.paint, + }, + }; + case 'fill': + // @ts-ignore + return { + ...props, + paint: { + ...layer.paint, + }, + }; + case 'line': + return { + ...props, + paint: { + // @ts-ignore + 'line-color': ['case'].concat(...colors).concat(['rgba(0, 0, 0, 0)']), + ...layer.paint, + }, + }; + } + + return null; +} diff --git a/features/Map/helpers/groupByProperty.tsx b/features/Map/helpers/groupByProperty.tsx new file mode 100644 index 00000000..a2925e6d --- /dev/null +++ b/features/Map/helpers/groupByProperty.tsx @@ -0,0 +1,8 @@ +import { FeatureCollection } from 'geojson'; +import groupBy from 'lodash/groupBy'; + +export function groupByProperty(geojson: FeatureCollection, property: string = 'type') { + return Object.entries(groupBy(geojson.features, (item) => item.properties[property])) + .map(([type, items]) => ({ type, count: items.length })) + .sort((a, b) => b.count - a.count); +} diff --git a/features/OKN/CardContent/CardContent.module.css b/features/OKN/CardContent/CardContent.module.css deleted file mode 100644 index 7352beca..00000000 --- a/features/OKN/CardContent/CardContent.module.css +++ /dev/null @@ -1,35 +0,0 @@ -.popup { - border-top-left-radius: inherit; - border-top-right-radius: inherit; -} - -.popup__content { - padding: 16px 8px 16px 16px; -} - -.popup__address { - margin: 8px 16px 0 0; - font-style: normal; - font-weight: 400; - line-height: 20px; - font-size: 16px; - color: #9baac3; -} - -.popup__imageLink { - display: flex; - border-top-left-radius: inherit; - border-top-right-radius: inherit; -} - -.popup__image { - display: block; - width: 100%; - height: auto; - min-height: 100px; - background-color: #2e2e2e; - border-top-left-radius: inherit; - border-top-right-radius: inherit; - object-fit: contain; - object-position: top center; -} diff --git a/features/OKN/CardContent/CardContent.tsx b/features/OKN/CardContent/CardContent.tsx deleted file mode 100644 index 1b34ec61..00000000 --- a/features/OKN/CardContent/CardContent.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import React, { useMemo } from 'react'; -import { Sources } from 'components/Card/components/Sources/Sources'; -import { ConstructionInfo } from 'components/Card/components/ConstructionInfo/ConstructionInfo'; -import { Header } from 'components/Card/components/Header/Header'; -import { Section } from 'components/Card/components/Section/Section'; -import { FeedbackButton } from 'features/FeedbackButton/FeedbackButton'; -import { SOURCES_BY_TYPE } from 'constants/sources'; -import { SourceType } from 'types/Sources.types'; -import { OknObject } from '../oknObject'; -import { OKNInfo } from './components/OKNInfo/OKNInfo'; -import styles from './CardContent.module.css'; - -export function OKNCardContent({ placemark }: { placemark: OknObject }) { - const { title, description } = useMemo(() => { - const indexOfComma = placemark?.properties.name?.indexOf(',') || -1; - - if (indexOfComma === -1) { - return { title: placemark?.properties.name }; - } - - return { - title: placemark?.properties.name.slice(0, indexOfComma), - description: placemark?.properties.name.slice(indexOfComma + 1), - }; - }, [placemark?.properties.name]); - - return placemark?.properties ? ( -
    - {placemark?.properties.img && ( - - {placemark?.properties.name} - - )} -
    -
    - {placemark?.properties.address && ( -
    - {placemark?.properties.address} -
    - )} - {placemark?.properties.date && ( -
    - -
    - )} -
    - -
    -
    - -
    - {placemark?.properties?.address && ( -
    - -
    - )} -
    -
    - ) : null; -} diff --git a/features/OKN/CardContent/components/OKNInfo/OKNInfo.tsx b/features/OKN/CardContent/components/OKNInfo/OKNInfo.tsx index bf835e4a..f5d989f7 100644 --- a/features/OKN/CardContent/components/OKNInfo/OKNInfo.tsx +++ b/features/OKN/CardContent/components/OKNInfo/OKNInfo.tsx @@ -2,8 +2,7 @@ import React, { useMemo } from 'react'; import classNames from 'classnames'; import { Tag } from 'ekb'; import { Info } from 'components/Card/components/Info/Info'; -import { Icon } from 'shared/UI/Icons'; -import { IconType } from 'shared/UI/Icons/Icons.types'; +import { SvgOknIcon } from 'features/OKN/SvgOknIcon'; import styles from './OKNInfo.module.css'; export type OKNInfoProps = { @@ -45,16 +44,9 @@ export function OKNInfo({ number, status }: OKNInfoProps) { [styles.oknInfo__oknLogo_lost]: isLost, })} > - + - {/*
    -
    - -
    -
    -
    19,6 МБ
    -
    */} ); } diff --git a/features/OKN/CardContent/index.ts b/features/OKN/CardContent/index.ts deleted file mode 100644 index 4342f0a8..00000000 --- a/features/OKN/CardContent/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { OKNCardContent } from './CardContent'; diff --git a/features/OKN/Filter/OknFilter.state.ts b/features/OKN/Filter/OknFilter.state.ts deleted file mode 100644 index 4efdad3f..00000000 --- a/features/OKN/Filter/OknFilter.state.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { OknAreaType, OknObjectSignificanceType } from '../oknConstants'; - -export interface AreaAction { - type: 'toggle'; - areaType: OknAreaType; -} -export interface ObjectsAction { - type: 'toggle'; - objectsType: OknObjectSignificanceType; -} - -export type AreaState = Record; -export type ObjectsState = Record; - -export function areaReducer(state: AreaState, action: AreaAction) { - switch (action.type) { - case 'toggle': - return { ...state, [action.areaType]: !state[action.areaType] }; - default: - return state; - } -} - -export const areaInitalState = Object.values(OknAreaType).reduce((acc, type) => { - acc[type] = false; - - return acc; -}, {} as AreaState); - -export function objectsReducer(state: ObjectsState, action: ObjectsAction) { - switch (action.type) { - case 'toggle': - return { ...state, [action.objectsType]: !state[action.objectsType] }; - default: - return state; - } -} - -export const objectsInitalState = Object.values(OknObjectSignificanceType).reduce((acc, type) => { - acc[type] = true; - - return acc; -}, {} as ObjectsState); diff --git a/features/OKN/Filter/OknFilter.tsx b/features/OKN/Filter/OknFilter.tsx index b538e073..1c9a7835 100644 --- a/features/OKN/Filter/OknFilter.tsx +++ b/features/OKN/Filter/OknFilter.tsx @@ -1,129 +1,34 @@ -import React, { useCallback, useEffect, useReducer, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { Checkbox, ListGrid, ListGridItem } from 'ekb'; -import { setFilter } from 'state/features/dataLayers'; -import { okn } from 'features/OKN/okn'; -import { FilterLoader } from 'components/Filters/FilterLoader'; -import { Section } from 'components/Card/components/Section/Section'; +import React from 'react'; +import { Divider } from 'ekb'; +import { OKN_ITEMS, ZONES_ITEMS } from 'features/OKN/okn'; import { FilterType } from 'types/Filters.types'; -import { activeFilterParamsSelector, activeFilterSelector } from 'state/features/selectors'; -import { AREA_CONFIG, OBJECTS_CONFIG } from '../Okn.constants'; -import { OknObjectSignificanceType, OknAreaType } from '../oknConstants'; -import { - areaInitalState, - areaReducer, - objectsInitalState, - objectsReducer, -} from './OknFilter.state'; - -type ObjectsCountEntries = [OknObjectSignificanceType, number][]; +import { LayerFilter } from 'features/Filters/LayerFilter'; +import { AREA_CONFIG, OBJECTS_CONFIG_COLORS, ZONES_COLORS } from '../Okn.constants'; + +const objects = OKN_ITEMS.map((item) => ({ + type: item.type, + subTitle: item.count, + color: OBJECTS_CONFIG_COLORS[item.type], +})); + +const zones = ZONES_ITEMS.map((item) => ({ + type: item.type, + subTitle: item.count, + color: ZONES_COLORS[item.type], + description: AREA_CONFIG[item.type].description, +})); export function OknFilter() { - const dispatch = useDispatch(); - const activeFilter = useSelector(activeFilterSelector); - const activeFilterParams = useSelector(activeFilterParamsSelector); - const [areaState, dispatchArea] = useReducer(areaReducer, areaInitalState); - const [objectsState, dispatchObjects] = useReducer(objectsReducer, objectsInitalState); - const [objectsCount, setObjectsCount] = useState(null); - const [areaCount, setAreaCount] = useState<[OknAreaType, number][]>(null); - - useEffect(() => { - okn.getObjectsCount().then((objectsCountResult: ObjectsCountEntries) => { - const sortedObjectsCount = objectsCountResult.sort( - ([, countA], [, countB]) => countB - countA, - ); - - setObjectsCount(sortedObjectsCount); - }); - }, []); - - useEffect(() => { - okn.getZonesCount().then((areaCountResult: [OknAreaType, number][]) => { - const sortedObjectsCount = areaCountResult.sort( - ([, countA], [, countB]) => countB - countA, - ); - - setAreaCount(sortedObjectsCount); - }); - }, []); - - const onAreaChange = useCallback( - (oknArea: OknAreaType) => dispatchArea({ type: 'toggle', areaType: oknArea }), - [], - ); - - const onObjectsChange = useCallback( - (oknObjectType: OknObjectSignificanceType) => - dispatchObjects({ type: 'toggle', objectsType: oknObjectType }), - [], - ); - - useEffect(() => { - dispatch( - setFilter({ - activeFilter: FilterType.OKN, - activeFilterParams: { - ...Object.entries(areaState).reduce( - (all, [id, value]) => ({ ...all, [id]: { value, type: 'area' } }), - {}, - ), - ...Object.entries(objectsState).reduce( - (all, [id, value]) => ({ ...all, [id]: { value, type: 'objects' } }), - {}, - ), - }, - }), - ); - }, [dispatch, areaState, objectsState]); - - if (activeFilter !== FilterType.OKN && !activeFilterParams.filter) { - return null; - } - - if (!(objectsCount && areaCount)) { - return ; - } - return ( <> - - {objectsCount?.map(([type, count]) => ( - onObjectsChange(type)} - /> - } - > - {type} - - ))} - - -
    - - {areaCount?.map(([type, count]) => ( - onAreaChange(type)} - /> - } - > - {type} - - ))} - -
    + + + ); } diff --git a/features/OKN/Okn.constants.ts b/features/OKN/Okn.constants.ts index e254db9b..78731e48 100644 --- a/features/OKN/Okn.constants.ts +++ b/features/OKN/Okn.constants.ts @@ -1,15 +1,9 @@ import { OknObjectSignificanceType, OknAreaType } from './oknConstants'; -export const OBJECTS_CONFIG = { - [OknObjectSignificanceType.Federal]: { - color: '#e65000', - }, - [OknObjectSignificanceType.Regional]: { - color: '#ae00ff', - }, - [OknObjectSignificanceType.Municipal]: { - color: '#03a600', - }, +export const OBJECTS_CONFIG_COLORS = { + [OknObjectSignificanceType.Federal]: '#e65000', + [OknObjectSignificanceType.Regional]: '#ae00ff', + [OknObjectSignificanceType.Municipal]: '#03a600', }; export const AREA_CONFIG = { @@ -28,3 +22,9 @@ export const AREA_CONFIG = { 'Временная зона в\u00A0100\u2013250 метров вокруг объекта, у\u00A0которого пока не\u00A0указана зона охраны', }, }; + +export const ZONES_COLORS = { + [OknAreaType.ObjectZone]: '#ff640f', + [OknAreaType.SecurityZone]: '#00b4ff', + [OknAreaType.ProtectZone]: '#e800b5', +}; diff --git a/features/OKN/OknCardContent.tsx b/features/OKN/OknCardContent.tsx new file mode 100644 index 00000000..12c840f4 --- /dev/null +++ b/features/OKN/OknCardContent.tsx @@ -0,0 +1,97 @@ +import React, { useMemo } from 'react'; +import { Card } from 'ekb'; +import { Sources } from 'components/Card/components/Sources/Sources'; +import { FeedbackButton } from 'features/FeedbackButton/FeedbackButton'; +import { SOURCES_BY_TYPE } from 'constants/sources'; +import { CardActions } from 'components/Card/components/CardActions'; +import { getYearNameByValue } from 'shared/helpers/getYearNameByValue'; +import { SourceType } from 'types/Sources.types'; +import { OknObject } from './oknObject'; +import { OKNInfo } from './CardContent/components/OKNInfo/OKNInfo'; + +export function OKNCardContent({ placemark }: { placemark: OknObject }) { + const { title, description } = useMemo(() => { + const indexOfComma = placemark?.properties.name?.indexOf(',') || -1; + + if (indexOfComma === -1) { + return { title: placemark?.properties.name }; + } + + return { + title: placemark?.properties.name.slice(0, indexOfComma), + description: placemark?.properties.name.slice(indexOfComma + 1), + }; + }, [placemark?.properties.name]); + + const constructionDateInfo = useMemo(() => { + const date = placemark?.properties.date; + const result = [ + { + type: 'value' as const, + title: 'Когда построили', + value: date, + }, + ]; + + const constructionYearMatch = date.match(/\d{4}/); + + if (constructionYearMatch) { + const constructionYear = Number(constructionYearMatch[0]); + const age = new Date().getFullYear() - Number(constructionYear); + + result.push({ + type: 'value' as const, + title: 'Возраст здания', + value: `${String(age)} ${getYearNameByValue(age)}`, + }); + } + + return result; + + return []; + }, [placemark?.properties.date]); + + if (!placemark?.properties) { + return null; + } + + return ( + + {placemark?.properties.name} + + } + actions={} + title={title} + description={description} + additionalInfo={[placemark?.properties.address].filter(Boolean)} + blocks={[ + ...constructionDateInfo, + { type: 'divider' as const }, + { + type: 'section', + value: ( + + ), + }, + { type: 'divider' as const }, + { + type: 'section' as const, + title: 'Источники', + value: , + }, + ]} + footerActions={} + /> + ); +} diff --git a/features/OKN/OknSource.tsx b/features/OKN/OknSource.tsx index 79005c76..b04fc66b 100644 --- a/features/OKN/OknSource.tsx +++ b/features/OKN/OknSource.tsx @@ -3,7 +3,7 @@ import { useSelector } from 'react-redux'; import React from 'react'; import type { CircleLayer, FillLayer, LineLayer } from 'react-map-gl'; import { activeFilterSelector, activeFilterParamsSelector } from 'state/features/selectors'; -import { AREA_CONFIG, OBJECTS_CONFIG } from 'features/OKN/Okn.constants'; +import { OBJECTS_CONFIG_COLORS, ZONES_COLORS } from 'features/OKN/Okn.constants'; import { FilterType } from 'types/Filters.types'; import { getLayerStyle } from 'features/Map/helpers/getFeatureState'; import { OknAreaType } from 'features/OKN/oknConstants'; @@ -11,13 +11,13 @@ import { MapItemType } from 'types/Content.types'; import useMapObjectState from 'features/Map/helpers/useMapObjectState'; import { useOpenMapItem } from 'features/Map/helpers/useOpenMapItem'; -const LAYERS = { - points: { - dataPath: '/ekb-okn.json', - layerId: 'ekb-okn-layer', - sourceId: 'ekb-okn-source', - zone: null, - }, +const OBJECTS_LAYER = { + dataPath: '/ekb-okn.json', + layerId: 'ekb-okn-layer', + sourceId: 'ekb-okn-source', +}; + +const ZONES_LAYERS = { protect: { dataPath: '/ekb-okn-protect.json', layerId: 'ekb-okn-protect-layer', @@ -42,39 +42,32 @@ export function OknSource() { const activeFilter = useSelector(activeFilterSelector); const activeFilterParams = useSelector(activeFilterParamsSelector); - useMapObjectState(LAYERS.points.layerId); - useMapObjectState(LAYERS.protect.layerId); - useMapObjectState(LAYERS.security.layerId); - useMapObjectState(LAYERS.objects.layerId); + useMapObjectState(OBJECTS_LAYER.layerId); + useMapObjectState(ZONES_LAYERS.protect.layerId); + useMapObjectState(ZONES_LAYERS.security.layerId); + useMapObjectState(ZONES_LAYERS.objects.layerId); - useOpenMapItem(LAYERS.points.layerId, MapItemType.OKN); + useOpenMapItem(OBJECTS_LAYER.layerId, MapItemType.OKN); if (activeFilter !== FilterType.OKN || !activeFilterParams) { return null; } - const activeItems = Object.entries(activeFilterParams) - // @ts-ignore - .filter(([, { value, type }]) => value && type === 'objects'); - - if (activeItems.length === 0) { - return null; - } - - const colors = activeItems.map(([category]) => [ - ['==', ['get', 'category'], category], - OBJECTS_CONFIG[category].color, - ]); + const colors = + activeFilterParams.okn?.objects?.map((category) => [ + ['==', ['get', 'category'], category], + OBJECTS_CONFIG_COLORS[category], + ]) || []; - const strokeColors = activeItems.map(([category]) => [ - ['==', ['get', 'category'], category], - '#000', - ]); + const strokeColors = + activeFilterParams.okn?.objects + ?.map((category) => [['==', ['get', 'category'], category], '#000']) + .filter((item) => item[1]) || []; - const layerStyle: CircleLayer = { - id: LAYERS.points.layerId, + const OBJECT_LAYER: CircleLayer = { + id: OBJECTS_LAYER.layerId, type: 'circle', - source: LAYERS.points.sourceId, + source: OBJECTS_LAYER.sourceId, paint: { 'circle-radius': getLayerStyle({ initial: 10, hover: 12, active: 13 }), // @ts-ignore @@ -86,11 +79,11 @@ export function OknSource() { }; const getZoneStyle = (type: string): FillLayer => ({ - id: LAYERS[type].layerId, + id: ZONES_LAYERS[type].layerId, type: 'fill', - source: LAYERS[type].sourceId, + source: ZONES_LAYERS[type].sourceId, paint: { - 'fill-color': AREA_CONFIG[LAYERS[type].zone].color, + 'fill-color': ZONES_COLORS[ZONES_LAYERS[type].zone], 'fill-opacity': getLayerStyle({ initial: 0.5, hover: 0.8, @@ -100,11 +93,11 @@ export function OknSource() { }); const getZoneOutlineStyle = (type: string): LineLayer => ({ - id: `${LAYERS[type].layerId}-outline`, + id: `${ZONES_LAYERS[type].layerId}-outline`, type: 'line', - source: LAYERS[type].sourceId, + source: ZONES_LAYERS[type].sourceId, paint: { - 'line-color': AREA_CONFIG[LAYERS[type].zone].color, + 'line-color': ZONES_COLORS[ZONES_LAYERS[type].zone], 'line-width': 3, 'line-dasharray': [2, 2], }, @@ -112,13 +105,13 @@ export function OknSource() { return ( <> - {Object.keys(LAYERS).map( + {Object.keys(ZONES_LAYERS).map( (layerKey, i) => - activeFilterParams[LAYERS[layerKey].zone]?.value && ( + activeFilterParams.okn?.zones?.includes(ZONES_LAYERS[layerKey].zone) && ( @@ -127,14 +120,16 @@ export function OknSource() { ), )} - - - + {activeFilterParams.okn?.objects?.length && ( + + + + )} ); } diff --git a/shared/UI/Icons/OKN.tsx b/features/OKN/SvgOknIcon.tsx similarity index 99% rename from shared/UI/Icons/OKN.tsx rename to features/OKN/SvgOknIcon.tsx index a5ad5eac..3616c023 100644 --- a/shared/UI/Icons/OKN.tsx +++ b/features/OKN/SvgOknIcon.tsx @@ -1,6 +1,6 @@ import React from 'react'; -export function OKN() { +export function SvgOknIcon() { return ( item.properties.category), +) + .map(([type, items]) => ({ type, count: items.length })) + .sort((a, b) => b.count - a.count); + +export const ZONES_ITEMS = oknMeta.zones + .map(([type, count]) => ({ type: type as string, count: count as number })) + .sort((a, b) => b.count - a.count); + export const okn = { getObject(id: string): Promise { return Promise.resolve( oknObjects.features.find((f) => String(f.properties.id) === id) as unknown as OknObject, ); }, - - async getObjectsCount() { - return Promise.resolve(oknMeta.points); - }, - - async getZonesCount() { - return Promise.resolve(oknMeta.zones); - }, }; diff --git a/features/OKN/oknObject.ts b/features/OKN/oknObject.ts index 2bde78d0..6d6b63b1 100644 --- a/features/OKN/oknObject.ts +++ b/features/OKN/oknObject.ts @@ -1,30 +1,26 @@ import { OknObjectSignificanceType } from './oknConstants'; export interface OknObject { - properties: OknProperties; - geometry: OknGeometry; -} - -export interface OknProperties { - id: number; - name: string; - address: string; - okn_number: string; - date: string; - type: string; - category: OknObjectSignificanceType; - img?: { url: string; title: string; preview?: string }; - document: { - archive: { id: number; url: string }; - date: string; + properties: { + id: number; name: string; - number: string; - }[]; - isExist?: string; - comment?: string; -} - -export interface OknGeometry { - coordinates: [lat: number, lng: number]; - type?: string; + address: string; + okn_number: string; + date: string; + type: string; + category: OknObjectSignificanceType; + img?: { url: string; title: string; preview?: string }; + document: { + archive: { id: number; url: string }; + date: string; + name: string; + number: string; + }[]; + isExist?: string; + comment?: string; + }; + geometry: { + coordinates: [lat: number, lng: number]; + type?: string; + }; } diff --git a/features/Quarter/CardContent/CardContent.module.css b/features/Quarter/CardContent/CardContent.module.css deleted file mode 100644 index 71958db5..00000000 --- a/features/Quarter/CardContent/CardContent.module.css +++ /dev/null @@ -1,30 +0,0 @@ -.popup { - border-top-left-radius: inherit; - border-top-right-radius: inherit; - padding: 16px 8px 16px 16px; -} - -.mainLink { - color: inherit; - font-size: inherit; - line-height: inherit; -} - -.description { - margin-top: 8px; - position: relative; - svg { - position: relative; - top: 2px; - left: 2px; - path { - fill: #000; - } - } - a { - border-radius: 8px; - font-size: 16px; - width: 100%; - text-align: center; - } -} diff --git a/features/Quarter/CardContent/CardContent.tsx b/features/Quarter/CardContent/CardContent.tsx deleted file mode 100644 index ab8a3d17..00000000 --- a/features/Quarter/CardContent/CardContent.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { Button, ButtonSize, ButtonType } from 'ekb'; -import { QuarterObject } from 'features/Quarter/quarterObject'; -import { Header } from 'components/Card/components/Header/Header'; -import { Info } from 'components/Card/components/Info/Info'; -import { Section } from 'components/Card/components/Section/Section'; -import sectionStyles from 'components/Card/components/Section/Section.module.css'; -import { Sources } from 'components/Card/components/Sources/Sources'; -import { FeedbackButton } from 'features/FeedbackButton/FeedbackButton'; -import { Icon } from 'shared/UI/Icons'; -import { IconType } from 'shared/UI/Icons/Icons.types'; -import { SOURCES_BY_TYPE } from 'constants/sources'; -import { SourceType } from 'types/Sources.types'; -import styles from './CardContent.module.css'; - -type QuarterCardContentProps = { - placemark?: QuarterObject; -}; - -export function QuarterCardContent({ placemark }: QuarterCardContentProps) { - if (!placemark) return null; - - return ( -
    -
    - -
    - -
    - -
    - -
    - -
    - -
    -
    -
    - -
    -
    -
    - ); -} diff --git a/features/Quarter/QuarterCardContent.tsx b/features/Quarter/QuarterCardContent.tsx new file mode 100644 index 00000000..a29dec00 --- /dev/null +++ b/features/Quarter/QuarterCardContent.tsx @@ -0,0 +1,51 @@ +import { Card, Button, ButtonSize, ButtonType, Icon, IconType } from 'ekb'; +import { Sources } from 'components/Card/components/Sources/Sources'; +import { FeedbackButton } from 'features/FeedbackButton/FeedbackButton'; +import { SOURCES_BY_TYPE } from 'constants/sources'; +import { SourceType } from 'types/Sources.types'; + +type QuarterCardContentProps = { + placemark?: QuarterObject; +}; + +interface QuarterObject { + districtTitle: string; + quarterTitle: string; + quarterDescription: string; + url: string; +} + +export function QuarterCardContent({ placemark }: QuarterCardContentProps) { + if (!placemark) return null; + + return ( + {}} + href={placemark.url} + > + Посмотреть телефон и почту квартального + + + ), + }, + { type: 'divider' }, + { type: 'value', title: 'Район', value: placemark.districtTitle }, + { type: 'divider' }, + { + type: 'section', + title: 'Источники', + value: , + }, + ]} + footerActions={} + /> + ); +} diff --git a/features/Quarter/Filter/QuarterFilter.tsx b/features/Quarter/QuarterFilter.tsx similarity index 80% rename from features/Quarter/Filter/QuarterFilter.tsx rename to features/Quarter/QuarterFilter.tsx index 0335da3d..4d92e371 100644 --- a/features/Quarter/Filter/QuarterFilter.tsx +++ b/features/Quarter/QuarterFilter.tsx @@ -1,9 +1,5 @@ -import { useEffect } from 'react'; -import { useDispatch } from 'react-redux'; import { Link, LinkSize } from 'ekb'; import styled from 'styled-components'; -import { setFilter } from 'state/features/dataLayers'; -import { FilterType } from 'types/Filters.types'; const Wrapper = styled.div` font-size: 14px; @@ -29,17 +25,6 @@ const Wrapper = styled.div` `; export function QuarterFilter() { - const dispatch = useDispatch(); - - useEffect(() => { - dispatch( - setFilter({ - activeFilter: FilterType.Quarter, - activeFilterParams: {}, - }), - ); - }); - return (

    diff --git a/features/Quarter/QuarterSource.tsx b/features/Quarter/QuarterSource.tsx deleted file mode 100644 index 6a458b28..00000000 --- a/features/Quarter/QuarterSource.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import React from 'react'; -import { FillLayer, Layer, LineLayer, Source } from 'react-map-gl'; -import { useSelector } from 'react-redux'; -import { activeFilterSelector } from 'state/features/selectors'; -import { FilterType } from 'types/Filters.types'; -import { getLayerStyle } from 'features/Map/helpers/getFeatureState'; -import { MapItemType } from 'types/Content.types'; -import useMapObjectState from 'features/Map/helpers/useMapObjectState'; -import { colorLuminance } from 'features/Map/helpers/colorLuminance'; -import { useOpenMapItem } from 'features/Map/helpers/useOpenMapItem'; - -const QUARTER_LAYER_ID = 'ekb-quarter-inspectors-layer'; -const QUARTER_SOURCE_ID = 'ekb-quarter-inspectors-source'; - -export function QuarterSource() { - const activeFilter = useSelector(activeFilterSelector); - - useMapObjectState(QUARTER_LAYER_ID); - - useOpenMapItem(QUARTER_LAYER_ID, MapItemType.Quarter); - - const layerStyle: FillLayer = { - type: 'fill', - id: QUARTER_LAYER_ID, - source: QUARTER_SOURCE_ID, - paint: { - 'fill-color': getLayerStyle({ - initial: '#9AADCC', - hover: colorLuminance('#9AADCC', 0.2), - active: colorLuminance('#9AADCC', 0.4), - }), - 'fill-opacity': 0.6, - }, - }; - - const layerStrokeStyle: LineLayer = { - id: `${QUARTER_LAYER_ID}-outline`, - type: 'line', - source: QUARTER_SOURCE_ID, - paint: { - 'line-color': '#000', - 'line-opacity': 0.5, - 'line-width': 1.5, - }, - }; - - if (activeFilter !== FilterType.Quarter) { - return null; - } - - return ( - - - - - ); -} diff --git a/features/Quarter/quarter.ts b/features/Quarter/quarter.ts index 35c08517..4bf3e6bf 100644 --- a/features/Quarter/quarter.ts +++ b/features/Quarter/quarter.ts @@ -1,11 +1,9 @@ import quarterObjects from 'public/ekb-quarters.json'; -import { QuarterObject } from './quarterObject'; export const quarter = { - getObject(quarterId: string): Promise { + getObject(quarterId: string): Promise { return Promise.resolve( - quarterObjects.features.find((f) => f.properties.id === quarterId) - .properties as unknown as QuarterObject, + quarterObjects.features.find((f) => f.properties.id === quarterId).properties, ); }, }; diff --git a/features/Quarter/quarterObject.ts b/features/Quarter/quarterObject.ts deleted file mode 100644 index ed648def..00000000 --- a/features/Quarter/quarterObject.ts +++ /dev/null @@ -1,6 +0,0 @@ -export interface QuarterObject { - districtTitle: string; - quarterTitle: string; - quarterDescription: string; - url: string; -} diff --git a/package.json b/package.json index a1c81ae0..0ae253ee 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,8 @@ "dependencies": { "@reduxjs/toolkit": "^2.0.1", "classnames": "^2.4.0", - "ekb": "1.2.0-rc.0", + "ekb": "1.2.0-rc.2", + "geojson": "^0.5.0", "lodash": "^4.17.21", "mapbox-gl": "npm:empty-npm-package@1.0.0", "maplibre-gl": "^3.6.2", @@ -30,11 +31,11 @@ "typescript": "^5.3.3" }, "devDependencies": { - "@typescript-eslint/eslint-plugin": "^6.16.0", - "@typescript-eslint/parser": "^6.16.0", "@types/lodash": "^4.14.202", "@types/node": "20.10.5", "@types/react": "^18.2.45", + "@typescript-eslint/eslint-plugin": "^6.16.0", + "@typescript-eslint/parser": "^6.16.0", "eslint": "^8.56.0", "eslint-config-next": "14.0.4", "eslint-config-prettier": "^9.1.0", diff --git a/pages/_document.tsx b/pages/_document.tsx index 65eaac43..07845291 100644 --- a/pages/_document.tsx +++ b/pages/_document.tsx @@ -41,13 +41,13 @@ export default function Document() { `, }} /> - )} + )} */} ); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 52561d7c..1ce0578c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,8 +12,11 @@ dependencies: specifier: ^2.4.0 version: 2.4.0 ekb: - specifier: 1.2.0-rc.0 - version: 1.2.0-rc.0(@types/react@18.2.45)(react-dom@18.2.0)(react@18.2.0) + specifier: 1.2.0-rc.2 + version: 1.2.0-rc.2(@types/react@18.2.45)(react-dom@18.2.0)(react@18.2.0) + geojson: + specifier: ^0.5.0 + version: 0.5.0 lodash: specifier: ^4.17.21 version: 4.17.21 @@ -2799,8 +2802,8 @@ packages: resolution: {integrity: sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==} dev: false - /ekb@1.2.0-rc.0(@types/react@18.2.45)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-XXLD+r8FIM5qYFZlArrKBQQSAcRb0zaWwJrGKpGgfROm0n6BZaNd9HfwiwW+JtVN9gz7YzxtdOU/iCoJVFqU7Q==} + /ekb@1.2.0-rc.2(@types/react@18.2.45)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-ySmX0NxsrkKRf/YOPqnyfqe68lTgJe4RJBdKSZJZwmoujzC9Dq6YM92PGVe7Za5Xd92ed0Zdkwj/SPvMe0BV0Q==} peerDependencies: react: ^18.2.0 react-dom: ^18.2.0 @@ -3410,6 +3413,11 @@ packages: resolution: {integrity: sha512-EvGQQi/zPrDA6zr6BnJD/YhwAkBP8nnJ9emh3EnHQKVMfg/MRVtPbMYdgVy/IaEmn4UfagD2a6fafPDL5hbtwg==} dev: false + /geojson@0.5.0: + resolution: {integrity: sha512-/Bx5lEn+qRF4TfQ5aLu6NH+UKtvIv7Lhc487y/c8BdludrCTpiWf9wyI0RTyqg49MFefIAvFDuEi5Dfd/zgNxQ==} + engines: {node: '>= 0.10'} + dev: false + /get-east-asian-width@1.2.0: resolution: {integrity: sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==} engines: {node: '>=18'} diff --git a/shared/UI/Icons/Auto.tsx b/shared/UI/Icons/Auto.tsx deleted file mode 100644 index b0941bdb..00000000 --- a/shared/UI/Icons/Auto.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react'; - -import { IconBaseProps } from './Icons.types'; - -export function Auto({ color, mix }: IconBaseProps) { - return ( - - - - ); -} diff --git a/shared/UI/Icons/Bike.tsx b/shared/UI/Icons/Bike.tsx deleted file mode 100644 index e91434b7..00000000 --- a/shared/UI/Icons/Bike.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react'; - -import { IconBaseProps } from './Icons.types'; - -export function Bike({ color, mix }: IconBaseProps) { - return ( - - - - ); -} diff --git a/shared/UI/Icons/BrokenHeart.tsx b/shared/UI/Icons/BrokenHeart.tsx deleted file mode 100644 index 3c7071c8..00000000 --- a/shared/UI/Icons/BrokenHeart.tsx +++ /dev/null @@ -1,47 +0,0 @@ -type Props = { - color: string; -}; - -function BrokenHeart({ color }: Props) { - return ( - - - - - - - - - - - - - - ); -} - -export default BrokenHeart; diff --git a/shared/UI/Icons/Bycicle.tsx b/shared/UI/Icons/Bycicle.tsx deleted file mode 100644 index b48aed3d..00000000 --- a/shared/UI/Icons/Bycicle.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react'; - -import { IconBaseProps } from './Icons.types'; - -export function Bycicle({ color, mix }: IconBaseProps) { - return ( - - - - ); -} diff --git a/shared/UI/Icons/Children.tsx b/shared/UI/Icons/Children.tsx deleted file mode 100644 index 824b7046..00000000 --- a/shared/UI/Icons/Children.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react'; - -import { IconBaseProps } from './Icons.types'; - -export function Children({ color, mix }: IconBaseProps) { - return ( - - - - ); -} diff --git a/shared/UI/Icons/Copy.tsx b/shared/UI/Icons/Copy.tsx deleted file mode 100644 index 058c796f..00000000 --- a/shared/UI/Icons/Copy.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react'; - -import { IconBaseProps } from './Icons.types'; - -export function Copy({ color, mix }: IconBaseProps) { - return ( - - - - - - - - - ); -} diff --git a/shared/UI/Icons/Download.tsx b/shared/UI/Icons/Download.tsx deleted file mode 100644 index 500292b1..00000000 --- a/shared/UI/Icons/Download.tsx +++ /dev/null @@ -1,20 +0,0 @@ -export function Download(props) { - return ( - - - - ); -} diff --git a/shared/UI/Icons/DownloadIcon.tsx b/shared/UI/Icons/DownloadIcon.tsx deleted file mode 100644 index 9941f4a3..00000000 --- a/shared/UI/Icons/DownloadIcon.tsx +++ /dev/null @@ -1,17 +0,0 @@ -export default function DownloadFacade({ color }: { color: string }) { - return ( - - - - ); -} diff --git a/shared/UI/Icons/Edit.tsx b/shared/UI/Icons/Edit.tsx deleted file mode 100644 index 590707ad..00000000 --- a/shared/UI/Icons/Edit.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import React from 'react'; -import { IconBaseProps } from 'shared/UI/Icons/Icons.types'; - -export function Edit({ color }: IconBaseProps) { - return ( - - - - - {' '} - {' '} - - - ); -} diff --git a/shared/UI/Icons/External.tsx b/shared/UI/Icons/External.tsx deleted file mode 100644 index 74db5453..00000000 --- a/shared/UI/Icons/External.tsx +++ /dev/null @@ -1,12 +0,0 @@ -export function External(props) { - return ( - - - - ); -} diff --git a/shared/UI/Icons/Heart.tsx b/shared/UI/Icons/Heart.tsx deleted file mode 100644 index de75a7b2..00000000 --- a/shared/UI/Icons/Heart.tsx +++ /dev/null @@ -1,35 +0,0 @@ -type Props = { - color: string; -}; - -function Heart({ color }: Props) { - return ( - - - - - - - - - - - - ); -} - -export default Heart; diff --git a/shared/UI/Icons/Icons.constants.ts b/shared/UI/Icons/Icons.constants.ts deleted file mode 100644 index 217c5f8a..00000000 --- a/shared/UI/Icons/Icons.constants.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Edit } from 'shared/UI/Icons/Edit'; -import { IconBaseProps, IconType } from './Icons.types'; -import { Copy } from './Copy'; -import { Link } from './Link'; -import { OKN } from './OKN'; -import { Pdf } from './Pdf'; -import { Auto } from './Auto'; -import { Pedestrian } from './Pedestrian'; -import { Bycicle } from './Bycicle'; -import { Bike } from './Bike'; -import { PublicTransport } from './PublicTransport'; -import { Children } from './Children'; -import { Download } from './Download'; -import { External } from './External'; - -export const ICON_BY_TYPE: Record JSX.Element> = { - [IconType.Copy]: Copy, - [IconType.Link]: Link, - [IconType.OKN]: OKN, - [IconType.Pdf]: Pdf, - [IconType.Auto]: Auto, - [IconType.Pedestrian]: Pedestrian, - [IconType.Bycicle]: Bycicle, - [IconType.Bike]: Bike, - [IconType.PublicTransport]: PublicTransport, - [IconType.Children]: Children, - [IconType.Edit]: Edit, - [IconType.Download]: Download, - [IconType.External]: External, -}; diff --git a/shared/UI/Icons/Icons.types.ts b/shared/UI/Icons/Icons.types.ts deleted file mode 100644 index 2c318bd3..00000000 --- a/shared/UI/Icons/Icons.types.ts +++ /dev/null @@ -1,17 +0,0 @@ -export enum IconType { - Link = 'link', - Copy = 'copy', - OKN = 'okn', - Pdf = 'pdf', - Auto = 'auto', - Pedestrian = 'pedestrian', - Bycicle = 'bycicle', - Bike = 'bike', - PublicTransport = 'publicTransport', - Children = 'children', - Edit = 'edit', - Download = 'download', - External = 'external', -} - -export type IconBaseProps = { color?: string; mix?: string }; diff --git a/shared/UI/Icons/Link.tsx b/shared/UI/Icons/Link.tsx deleted file mode 100644 index 0eae3e4d..00000000 --- a/shared/UI/Icons/Link.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react'; - -import { IconBaseProps } from './Icons.types'; - -export function Link({ color, mix }: IconBaseProps) { - return ( - - - - ); -} diff --git a/shared/UI/Icons/Pdf.tsx b/shared/UI/Icons/Pdf.tsx deleted file mode 100644 index d20a5c84..00000000 --- a/shared/UI/Icons/Pdf.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import React from 'react'; - -import { IconBaseProps } from './Icons.types'; - -export function Pdf({ color, mix }: IconBaseProps) { - return ( - - - - - - - - - - - - - ); -} diff --git a/shared/UI/Icons/Pedestrian.tsx b/shared/UI/Icons/Pedestrian.tsx deleted file mode 100644 index c14ad5ab..00000000 --- a/shared/UI/Icons/Pedestrian.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react'; - -import { IconBaseProps } from './Icons.types'; - -export function Pedestrian({ color, mix }: IconBaseProps) { - return ( - - - - ); -} diff --git a/shared/UI/Icons/PublicTransport.tsx b/shared/UI/Icons/PublicTransport.tsx deleted file mode 100644 index 83ff0132..00000000 --- a/shared/UI/Icons/PublicTransport.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react'; - -import { IconBaseProps } from './Icons.types'; - -export function PublicTransport({ color, mix }: IconBaseProps) { - return ( - - - - ); -} diff --git a/shared/UI/Icons/index.tsx b/shared/UI/Icons/index.tsx deleted file mode 100644 index 53abf79b..00000000 --- a/shared/UI/Icons/index.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import React, { useMemo } from 'react'; - -import { ICON_BY_TYPE } from './Icons.constants'; -import { IconType, IconBaseProps } from './Icons.types'; - -type IconProps = IconBaseProps & { - type: IconType; -}; - -export function Icon({ type, ...baseProps }: IconProps) { - const IconComponent = useMemo(() => ICON_BY_TYPE[type], [type]); - - return ; -} diff --git a/state/config.ts b/state/config.ts new file mode 100644 index 00000000..88c36f80 --- /dev/null +++ b/state/config.ts @@ -0,0 +1,262 @@ +import { CircleLayer, FillLayer, LineLayer } from 'react-map-gl'; +import { getLayerStyle } from 'features/Map/helpers/getFeatureState'; +import { colorLuminance } from 'features/Map/helpers/colorLuminance'; +import { FilterType } from 'types/Filters.types'; +import { MapItemType } from 'types/Content.types'; + +interface Source { + id: string; + fields: Record; +} + +export interface IVisualisationLayer { + id: string; + type: string; + path: string; + source: Source['id']; + property: string; + values: Record< + string, + { + color?: string; + } + >; + paint: CircleLayer['paint'] | LineLayer['paint'] | FillLayer['paint']; + openable?: boolean; +} + +interface Filter { + id: string; + type: string; + property: string; + path: string; + filterVisualisationLayers: IVisualisationLayer['id'][]; + values: Record< + string, + { + color?: string; + description?: string; + } + >; +} + +export interface ILayer { + id: string; + filters: Filter['id'][]; + visualisationLayers: IVisualisationLayer['id'][]; + active: boolean; +} + +interface App { + sources: Record; + layers: Record; + filters: Record; + visualisationLayers: Record; +} + +export const state: App = { + sources: { + 'ekb-lines-source': { + id: 'ekb-lines-source', + fields: {}, + }, + 'ekb-points-source': { + id: 'ekb-lines', + fields: {}, + }, + 'ekb-quarter-source': { + id: 'ekb-quarter-source', + fields: {}, + }, + 'ekb-designcode-source': { + id: 'ekb-designcode-source', + fields: {}, + }, + }, + layers: { + [MapItemType.OKN]: { + id: MapItemType.OKN, + visualisationLayers: [], + filters: [], + active: false, + }, + [MapItemType.DesignCode]: { + id: MapItemType.DesignCode, + visualisationLayers: ['ekbDesignCodeLayer'], + filters: [FilterType.DesignCode], + active: false, + }, + [MapItemType.DTP]: { + id: MapItemType.DTP, + visualisationLayers: [], + filters: ['dtpSeverity', 'dtpParticipants'], + active: false, + }, + [MapItemType.LinePoint]: { + id: MapItemType.LinePoint, + visualisationLayers: ['ekbPointsLayer', 'ekbLinesLayer'], + filters: [FilterType.Line], + active: false, + }, + [MapItemType.Quarter]: { + id: MapItemType.Quarter, + visualisationLayers: ['ekbQuarterLayer', 'ekbQuarterLayerStroke'], + filters: [], + active: false, + }, + }, + filters: { + dtpSeverity: { + id: 'dtpSeverity', + type: 'checkboxes', + filterVisualisationLayers: [], + path: '/ekb-dtp.json', + property: 'severity', + values: { + Легкий: { color: '#36ccaa' }, + Тяжёлый: { color: '#fdcf4e' }, + 'С погибшими': { color: '#ff0000' }, + }, + }, + dtpParticipants: { + id: 'dtpSeverity', + type: 'checkboxes', + filterVisualisationLayers: [], + path: '/ekb-dtp.json', + property: 'participants', // TODO: array + values: {}, + }, + [FilterType.Line]: { + id: FilterType.Line, + type: 'checkboxes', + filterVisualisationLayers: ['ekbPointsLayer', 'ekbLinesLayer'], + path: '/ekb-color-points.json', + property: 'type', + values: { + 'Красная линия': { + color: '#e31e24', + description: 'Маршрут по\u00A0историческому центру города', + }, + 'Синяя линия': { + color: '#189eda', + description: 'Маршрут по\u00A0местам, связанным с\u00A0царской семьей', + }, + 'Фиолетовая линия': { + color: '#9747ff', + description: 'Арт-объекты фестиваля уличного искусства «Стенограффия»', + }, + }, + }, + [FilterType.DesignCode]: { + id: FilterType.DesignCode, + type: 'checkboxes', + filterVisualisationLayers: ['ekbDesignCodeLayer'], + path: '/ekb-design-code.json', + property: 'type', + values: { + 'Обычные адресные таблички': { color: '#ff640a' }, + 'Таблички ЧО': { color: '#e63223' }, + 'Памятные таблички': { color: '#f758b6' }, + 'Исторические адресные таблички': { color: '#aa9b46' }, + 'Логотипы и айдентика': { color: '#00b400' }, + 'Навигационные стелы': { color: '#ffd400' }, + 'Таблички ОКН': { color: '#00b4ff' }, + 'Фризы остановок': { color: '#55647d' }, + 'Уличная мебель': { color: '#5820e4' }, + Светофор: { color: '#965a14' }, + Транспорт: { color: '#006d4e' }, + 'Настенные таблички': { color: '#a00041' }, + 'Столбы со стрелками': { color: '#86e621' }, + }, + }, + }, + visualisationLayers: { + ekbLinesLayer: { + id: 'ekbLinesLayer', + path: '/ekb-color-lines.json', + type: 'line', + source: 'ekb-lines-source', + property: 'type', + values: { + 'Красная линия': { color: '#e31e24' }, + 'Синяя линия': { color: '#189eda' }, + 'Фиолетовая линия': { color: '#9747ff' }, + }, + paint: { + 'line-width': 3, + }, + }, + ekbPointsLayer: { + id: 'ekbPointsLayer', + path: '/ekb-color-points.json', + type: 'circle', + source: 'ekb-points-source', + property: 'type', + openable: true, + values: { + 'Красная линия': { color: '#e31e24' }, + 'Синяя линия': { color: '#189eda' }, + 'Фиолетовая линия': { color: '#9747ff' }, + }, + paint: { + 'circle-stroke-width': 1, + 'circle-radius': getLayerStyle({ initial: 8, hover: 10, active: 12 }), + }, + }, + ekbQuarterLayer: { + id: 'ekbQuarterLayer', + path: '/ekb-quarters.json', + type: 'fill', + source: 'ekb-quarter-source', + openable: true, + property: '', + values: {}, + paint: { + 'fill-color': getLayerStyle({ + initial: '#9AADCC', + hover: colorLuminance('#9AADCC', 0.2), + active: colorLuminance('#9AADCC', 0.4), + }), + 'fill-opacity': 0.6, + }, + }, + ekbQuarterLayerStroke: { + id: 'ekbQuarterLayerStroke', + path: '/ekb-quarters.json', + type: 'line', + source: 'ekb-quarter-source', + openable: true, + property: '', + values: {}, + paint: { + 'line-color': '#000', + 'line-opacity': 0.5, + 'line-width': 1.5, + }, + }, + ekbDesignCodeLayer: { + id: 'ekbDesignCodeLayer', + path: '/ekb-design-code.json', + type: 'marker-image', + source: 'ekb-designcode-source', + property: 'type', + openable: true, + values: { + 'Обычные адресные таблички': { color: '#ff640a' }, + 'Таблички ЧО': { color: '#e63223' }, + 'Памятные таблички': { color: '#f758b6' }, + 'Исторические адресные таблички': { color: '#aa9b46' }, + 'Логотипы и айдентика': { color: '#00b400' }, + 'Навигационные стелы': { color: '#ffd400' }, + 'Таблички ОКН': { color: '#00b4ff' }, + 'Фризы остановок': { color: '#55647d' }, + 'Уличная мебель': { color: '#5820e4' }, + Светофор: { color: '#965a14' }, + Транспорт: { color: '#006d4e' }, + 'Настенные таблички': { color: '#a00041' }, + 'Столбы со стрелками': { color: '#86e621' }, + }, + paint: {}, + }, + }, +}; diff --git a/state/features/dataLayers.ts b/state/features/dataLayers.ts index 22883ae4..07b51e87 100644 --- a/state/features/dataLayers.ts +++ b/state/features/dataLayers.ts @@ -1,11 +1,23 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; - import { getFilterTypeFromHash } from 'shared/helpers/hash'; -import { SetFilterParamsPayload, SetFilterPayload, State, ToggleDataPayload } from 'state/state'; +import { State } from 'state/state'; import { FilterType } from 'types/Filters.types'; +export interface SetFilterPayload { + activeFilter: string; + activeFilterParams: any; +} + +export interface SetFilterParamsPayload { + activeFilterParams: any; +} + +export interface ToggleDataPayload { + type: string; +} + export const initialState: State['dataLayer'] = { - activeFilter: (getFilterTypeFromHash() as FilterType) || Object.values(FilterType)[0], + activeFilter: (getFilterTypeFromHash() as string) || Object.values(FilterType)[0], activeFilterParams: null, }; @@ -22,6 +34,21 @@ const dataLayerSlice = createSlice({ const { activeFilterParams } = action.payload; state.activeFilterParams = activeFilterParams; }, + updateFilterParams( + state, + action: PayloadAction<{ + activeFilter: string; + activeFilterParams: any; + }>, + ) { + const { activeFilterParams, activeFilter } = action.payload; + state.activeFilter = activeFilter; + state.activeFilterParams = state.activeFilterParams || {}; + state.activeFilterParams[activeFilter] = { + ...state.activeFilterParams[activeFilter], + ...activeFilterParams[activeFilter], + }; + }, toggleData(state, action: PayloadAction) { const { type } = action.payload; @@ -31,6 +58,7 @@ const dataLayerSlice = createSlice({ }, }); -export const { toggleData, setFilter, setFilterParams } = dataLayerSlice.actions; +export const { toggleData, setFilter, setFilterParams, updateFilterParams } = + dataLayerSlice.actions; export const dataLayerReducer = dataLayerSlice.reducer; diff --git a/state/state.ts b/state/state.ts index 9e374e92..49cf29b5 100644 --- a/state/state.ts +++ b/state/state.ts @@ -1,41 +1,6 @@ -import { FilterType } from 'types/Filters.types'; -import { OknAreaType } from 'features/OKN/oknConstants'; -import { LineObject, LineType } from 'features/Lines/lineType'; - -import { MapItemType } from 'types/Content.types'; - -export interface LinesData { - lines: { - type: LineType; - id: number; - }[]; - points: { - type: LineType; - data: LineObject[]; - }[]; -} - -export interface LinesState { - data: LinesData; - mapItemType: MapItemType; -} - export interface State { dataLayer: { - activeFilter: FilterType | OknAreaType; + activeFilter: string; activeFilterParams: any; }; } - -export interface SetFilterPayload { - activeFilter: FilterType | OknAreaType; - activeFilterParams: any; -} - -export interface SetFilterParamsPayload { - activeFilterParams: any; -} - -export interface ToggleDataPayload { - type: FilterType | OknAreaType; -} diff --git a/types/Content.types.ts b/types/Content.types.ts index ebb829cc..2daa62d8 100644 --- a/types/Content.types.ts +++ b/types/Content.types.ts @@ -1,10 +1,10 @@ export enum MapItemType { - DesignCode = 'designCode', + DesignCode = 'ekbDesignCodeLayer', DTP = 'dtp', Houses = 'houses', - LinePoint = 'LinePoint', + LinePoint = 'ekbPointsLayer', OKN = 'okn', - Quarter = 'quarter', + Quarter = 'ekbQuarterLayer', } export type ContentConfig = Record< diff --git a/types/Filters.types.ts b/types/Filters.types.ts index 2a4c5a8e..78ed6f6b 100644 --- a/types/Filters.types.ts +++ b/types/Filters.types.ts @@ -11,6 +11,7 @@ export enum FilterType { HouseFloor = 'houseFloor', HouseWearTear = 'houseWearTear', OKN = 'okn', + // OknZones = 'oknZones', DesignCode = 'designCode', DTP = 'dtp', Line = 'line', @@ -20,7 +21,7 @@ export enum FilterType { export interface FilterConfigItem { title: string; - component: ReactNode; + component?: ReactNode; source?: Source[]; isVerified: boolean; }