diff --git a/README.md b/README.md index 201cb17690..6f548f841c 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,8 @@ documentation of the [app](/packages/app/README.md) and (optionally the) Without describing in detail all the rules we tend to follow here are some worth noting: +#### General + - All filenames are written in kebab-case. - We use named exports where possible. They improve typing and help refactoring. - We aim to stop using barrel files (using an index file in a folder to bundle exports for the consuming code). @@ -91,12 +93,21 @@ noting: - We avoid using `boolean && doSomething();` inside the component's JavaScript logic, but do use it inside the component's JSX (`{boolean && ( ... )}`) to conditionally render (parts of) the component. - We avoid unnecessary short-hand variable names like `arr` for array or `i` for index or `acc` for a `reduce` accumulator. + +- Completely separate Javascript logic from HTML/JSX. This means also remove maps from the JSX. Additionally, if you have nested maps extract them into components passing the required data to map to the component. +- We prefer early returns. If statements should be on multiple lines, so no single line if statements. + +#### Styling - We write Styled Components using its OOTB tagged template literal functions instead of using an additional layer of the Styled System's `css()` method. This method improves readability, makes code easier to understand and sticks to the fundamentals of CSS. This method still allows for usage of Styled System's theme definitions, yet removes a dependency on the actual package. - We included a `Styled`-prefix when creating Styled Components. This makes them easily distinguishable from other components. Examples would be `StyledInput` or `StyledTile`. - We avoid using magic numbers in code, be it logic, JSX or styles. Magic numbers are often derived from the theme defined by Styled System and resolve to properties such as spacing and font-sizes, but are unclear on its own. Instead, we import the desired property and refer to the index in that properties array. An example would be `padding: 3` (undesired) vs `padding: space[3]` (desired). + +#### GIT + - We do not have a hard preference or requirement for using `git rebase` or `git merge`. Developers should follow what works best for them, but it should be noted that both methods are allowed and actively used in this project. - We do not have a hard preference or requirement for squashing a multitude of git commits, but it can be useful to apply this when creating a pull request. This action should be used on an 'as-needed basis': if a pull request grows large due to a large amount of commits, it might improve reviewability when multiple commits are squashed. It should be noted that pull requests are squashed when merged, aside from pull requests to `master`. This is to keep a clear view of features and fixes that were merged as part of a release. - Continuing on the above: we should write a comprehensive commit message when squash merging a pull request. This message should be a (filtered) summary of the commits on the branch. + - We use the following branch names: - `feature/COR-XXX-descriptive-name-of-ticket-branch` for features - `bugfix/COR-XXX-descriptive-name-of-ticket-branch` for bug fixes diff --git a/packages/app/schema/topical/icon.json b/packages/app/schema/topical/icon.json index 9af0334a15..36b96d0c0e 100644 --- a/packages/app/schema/topical/icon.json +++ b/packages/app/schema/topical/icon.json @@ -7,6 +7,7 @@ "AlcoholVerkoop", "Archive", "Arrow", + "ArrowWithIntensity", "Arts", "Avondklok", "BarChart", diff --git a/packages/app/src/components/article-detail.tsx b/packages/app/src/components/article-detail.tsx index 06f13790e3..9ceda5a756 100644 --- a/packages/app/src/components/article-detail.tsx +++ b/packages/app/src/components/article-detail.tsx @@ -3,7 +3,7 @@ import styled from 'styled-components'; import { ArrowIconLeft } from '~/components/arrow-icon'; import { Box } from '~/components/base'; import { ContentBlock } from '~/components/cms/content-block'; -import { Heading, InlineText } from '~/components/typography'; +import { Heading, InlineText, Anchor } from '~/components/typography'; import { ArticleCategoryType } from '~/domain/topical/common/categories'; import { useIntl } from '~/intl'; import { SiteText } from '~/locale'; @@ -15,6 +15,8 @@ import { RichContent } from './cms/rich-content'; import { LinkWithIcon } from './link-with-icon'; import { PublicationDate } from './publication-date'; import { useBreakpoints } from '~/utils/use-breakpoints'; +import { colors } from '@corona-dashboard/common'; +import { space } from '~/style/theme'; interface ArticleDetailProps { article: Article; text: SiteText['pages']['topical_page']['shared']; @@ -50,29 +52,17 @@ export function ArticleDetail({ article, text }: ArticleDetailProps) { - + {!breakpoints.xs ? article.imageMobile && ( - + ) : article.imageDesktop && ( - + )} {!!article.content?.length && ( @@ -93,9 +83,7 @@ export function ArticleDetail({ article, text }: ArticleDetailProps) { {article.categories && ( - - {text.secties.artikelen.tags} - + {text.secties.artikelen.tags} - - { - text.secties.artikelen.categorie_filters[ - item as ArticleCategoryType - ] - } - + {text.secties.artikelen.categorie_filters[item as ArticleCategoryType]} ))} @@ -134,25 +116,26 @@ export function ArticleDetail({ article, text }: ArticleDetailProps) { ); } -const TagAnchor = styled.a( - css({ - display: 'block', - border: '2px solid transparent', - mb: 3, - px: 3, - py: 2, - backgroundColor: 'blue3', - color: 'blue8', - textDecoration: 'none', - transition: '0.1s border-color', +const StyledTagAnchor = styled(Anchor)` + border-radius: 5px; + border: 2px solid ${colors.gray4}; + color: ${colors.black}; + display: block; + margin-bottom: ${space[3]}; + padding: ${space[2]} ${space[3]}; - '&:hover': { - borderColor: 'blue8', - }, + &:focus:focus-visible { + outline: 2px dotted ${colors.blue8}; + } - '&:focus': { - outline: '2px dotted', - outlineColor: 'blue8', - }, - }) -); + &:hover { + background: ${colors.blue8}; + border: 2px solid ${colors.blue8}; + color: ${colors.white}; + text-shadow: 0.5px 0px 0px ${colors.white}, -0.5px 0px 0px ${colors.white}; + + &:focus-visible { + outline: 2px dotted ${colors.magenta3}; + } + } +`; diff --git a/packages/app/src/components/choropleth/components/canvas-choropleth-map.tsx b/packages/app/src/components/choropleth/components/canvas-choropleth-map.tsx index b6f29e0142..c33d90d710 100644 --- a/packages/app/src/components/choropleth/components/canvas-choropleth-map.tsx +++ b/packages/app/src/components/choropleth/components/canvas-choropleth-map.tsx @@ -16,7 +16,7 @@ import type { AnchorEventHandler } from './choropleth-map'; Konva.pixelRatio = typeof window !== 'undefined' ? Math.min(window.devicePixelRatio, 2) : 1; -export type CanvasChoroplethMapProps = { +export interface CanvasChoroplethMapProps { anchorEventHandlers: AnchorEventHandler; annotations: AccessibilityAnnotations; choroplethFeatures: ChoroplethFeatures; @@ -33,7 +33,7 @@ export type CanvasChoroplethMapProps = { mapProjection: () => GeoProjection; tooltipTrigger: ChoroplethTooltipHandlers[2]; width: number; -}; +} /** * This is one transparent pixel encoded in a dataUrl. This is used for the image overlay on top of the canvas that @@ -181,12 +181,12 @@ export const CanvasChoroplethMap = (props: CanvasChoroplethMapProps) => { ); }; -type HighlightedFeatureProps = { +interface HighlightedFeatureProps { feature: [number, number][][] | undefined; featureProps: FeatureProps; code: string | undefined; hoverCode: string | undefined; -}; +} const HighlightedFeature = memo((props: HighlightedFeatureProps) => { const { feature, featureProps, code, hoverCode } = props; @@ -212,13 +212,13 @@ const HighlightedFeature = memo((props: HighlightedFeatureProps) => { ); }); -type HoveredFeatureProps = { +interface HoveredFeatureProps { hoveredRef: RefObject; hover: [number, number][][] | undefined; hoverCode: string | undefined; featureProps: FeatureProps; isKeyboardActive?: boolean; -}; +} const HoveredFeature = memo((props: HoveredFeatureProps) => { const { hoveredRef, hover, hoverCode, featureProps, isKeyboardActive } = props; @@ -227,30 +227,58 @@ const HoveredFeature = memo((props: HoveredFeatureProps) => { return null; } + /** + * The code in the condition below is a workaround. + * + * This is required as for some reason, the water bodies also get rendered as a Feature (you can see another fix for this in the + * Features component below). As a consequence, when making the fix introduced in COR-1149 which required adding an additional + * line to the HoveredFeature, the water bodies ended up receiving the same fill colour as the land around them and thereby masking + * the white water body. + * + * To fix this, there are now two maps iterating over two arrays for Zeeland. One represents land and the other, water. + */ + let landCoords: [number, number][][] = [...hover]; + let waterCoords: [number, number][][] | undefined; + if (hoverCode === 'VR19') { + landCoords = hover.filter((_, index) => index === 0 || index === 5); + waterCoords = hover.filter((_, index) => !(index === 0 || index === 5)); + } + return ( - {hover.map((x, i) => ( - + {landCoords.map((coordinates, index) => ( + <> + + {/* The additional line is used as an overlay on the original to make it seem like the stroke on the original line is on the outside */} + + > + ))} + {waterCoords?.map((coordinates, index) => ( + ))} ); }); -type OutlinesProps = { +interface OutlinesProps { geoInfo: ProjectedGeoInfo[]; featureProps: FeatureProps; -}; +} const Outlines = memo((props: OutlinesProps) => { const { geoInfo, featureProps } = props; @@ -276,11 +304,11 @@ const Outlines = memo((props: OutlinesProps) => { ); }); -type FeaturesProps = { +interface FeaturesProps { geoInfo: ProjectedGeoInfo[]; featureProps: FeatureProps; children: React.ReactNode; -}; +} const Features = memo((props: FeaturesProps) => { const { geoInfo, featureProps, children } = props; @@ -336,7 +364,7 @@ const Features = memo((props: FeaturesProps) => { ); }); -type AreaMapProps = { +interface AreaMapProps { isTabInteractive: boolean; geoInfo: ProjectedGeoInfo[]; getLink?: (code: string) => string; @@ -347,7 +375,7 @@ type AreaMapProps = { handleMouseOver: (event: MouseEvent) => void; height: number; width: number; -}; +} type GeoInfoGroup = { code: string; @@ -383,6 +411,7 @@ function AreaMap(props: AreaMapProps) { = (code: string) => T; -type GetHoverFeatureProp = ( - code: string, - isActivated?: boolean, - isKeyboardActive?: boolean -) => T; +type GetHoverFeatureProp = (code: string, isActivated?: boolean, isKeyboardActive?: boolean) => T; export type FeatureProps = { /** @@ -39,7 +35,7 @@ type FeaturePropFunctions = { export const DEFAULT_STROKE_WIDTH = 0.5; -export const DEFAULT_HOVER_STROKE_WIDTH = 3; +export const DEFAULT_HOVER_STROKE_WIDTH = 6; /** * This hook returns the visual props for the map features based on the specified map type. @@ -60,10 +56,7 @@ export function useFeatureProps( dataOptions: DataOptions, dataConfig: DataConfig ): FeatureProps { - return useMemo( - () => getFeatureProps(map, getFillColor, dataOptions, dataConfig), - [map, getFillColor, dataOptions, dataConfig] - ); + return useMemo(() => getFeatureProps(map, getFillColor, dataOptions, dataConfig), [map, getFillColor, dataOptions, dataConfig]); } export function getFeatureProps( @@ -83,38 +76,17 @@ export function getFeatureProps( strokeWidth: () => dataConfig.areaStrokeWidth, }, hover: { - fill: (_code: string, isHover?: boolean) => - isHover ? dataConfig.hoverFill : 'none', + fill: (_code: string) => getFillColor(_code), stroke: !dataOptions.highlightSelection || !dataOptions.selectedCode - ? ( - _code: string, - isActivated?: boolean, - isKeyboardActive?: boolean - ) => - isActivated - ? isKeyboardActive - ? dataConfig.highlightStroke - : dataConfig.hoverStroke - : 'none' + ? (_code: string, isActivated?: boolean, isKeyboardActive?: boolean) => + isActivated ? (isKeyboardActive ? dataConfig.highlightStroke : dataConfig.hoverStroke) : 'none' : (code: string, isActivated?: boolean) => - code === dataOptions.selectedCode - ? isActivated - ? dataConfig.hoverStroke - : dataConfig.highlightStroke - : isActivated - ? dataConfig.hoverStroke - : 'none', + code === dataOptions.selectedCode ? (isActivated ? dataConfig.hoverStroke : dataConfig.highlightStroke) : isActivated ? dataConfig.hoverStroke : 'none', strokeWidth: !dataOptions.highlightSelection || !dataOptions.selectedCode - ? (_code: string, isActivated?: boolean) => - isActivated ? dataConfig.hoverStrokeWidth : 0 - : (code: string, isActivated?: boolean) => - code === dataOptions.selectedCode - ? dataConfig.highlightStrokeWidth - : isActivated - ? dataConfig.hoverStrokeWidth - : 0, + ? (_code: string, isActivated?: boolean) => (isActivated ? dataConfig.hoverStrokeWidth : 0) + : (code: string, isActivated?: boolean) => (code === dataOptions.selectedCode ? dataConfig.highlightStrokeWidth : isActivated ? dataConfig.hoverStrokeWidth : 0), }, outline: { fill: () => 'transparent', @@ -133,19 +105,10 @@ export function getFeatureProps( strokeWidth: () => dataConfig.areaStrokeWidth, }, hover: { - fill: () => 'none', - stroke: ( - _code: string, - isActivated?: boolean, - isKeyboardActive?: boolean - ) => - isActivated - ? isKeyboardActive - ? dataConfig.highlightStroke - : dataConfig.hoverStroke - : 'none', - strokeWidth: (_code: string, isActivated?: boolean) => - isActivated ? dataConfig.hoverStrokeWidth : 0, + fill: (_code: string) => getFillColor(_code), + stroke: (_code: string, isActivated?: boolean, isKeyboardActive?: boolean) => + isActivated ? (isKeyboardActive ? dataConfig.highlightStroke : dataConfig.hoverStroke) : 'none', + strokeWidth: (_code: string, isActivated?: boolean) => (isActivated ? dataConfig.hoverStrokeWidth : 0), }, outline: { fill: () => 'none', diff --git a/packages/app/src/components/choropleth/tooltips/tooltip-content.tsx b/packages/app/src/components/choropleth/tooltips/tooltip-content.tsx index 048307e4c3..3005e20e95 100644 --- a/packages/app/src/components/choropleth/tooltips/tooltip-content.tsx +++ b/packages/app/src/components/choropleth/tooltips/tooltip-content.tsx @@ -1,107 +1,79 @@ -import { Location } from '@corona-dashboard/icons'; -import css from '@styled-system/css'; -import { ReactNode } from 'react'; +import { colors } from '@corona-dashboard/common'; +import { ChevronRight, Location } from '@corona-dashboard/icons'; +import { MouseEventHandler, ReactNode } from 'react'; import styled from 'styled-components'; -import { Text } from '~/components/typography'; +import { Box } from '~/components/base'; +import { InlineText, Text } from '~/components/typography'; import { space } from '~/style/theme'; import { useIsTouchDevice } from '~/utils/use-is-touch-device'; -interface IProps { +interface TooltipContentProps { title: string; - onSelect?: (event: React.MouseEvent) => void; link?: string; children?: ReactNode; } -export function TooltipContent(props: IProps) { - const { title, onSelect, link, children } = props; +export const TooltipContent = ({ title, link, children }: TooltipContentProps) => { const isTouch = useIsTouchDevice(); return ( - - + + {title} - {(onSelect || link) && } - - {children && {children}} + {link && } + + + {children && {children}} ); -} +}; -const StyledTooltipContent = styled.div<{ isInteractive: boolean }>((x) => - css({ - color: 'black', - width: '100%', - minWidth: 250, - borderRadius: 1, - cursor: x.onClick ? 'pointer' : 'default', - pointerEvents: x.isInteractive ? undefined : 'none', - }) -); - -function TooltipHeader({ href, children }: { href?: string; children: ReactNode }) { - if (href) { - return ( - - {children} - - ); - } - - return {children}; +interface StyledTooltipContentProps { + isInteractive: boolean; + onClick?: MouseEventHandler; } -const StyledTooltipHeader = styled.div( - css({ - whiteSpace: 'nowrap', - color: 'black', - py: 2, - px: 3, - display: 'flex', - justifyContent: 'space-between', - alignItems: 'center', - textDecoration: 'none!important', - }) -); +const StyledTooltipContent = styled(Box)` + color: ${colors.black}; + display: flex; + flex-direction: column; + min-width: 250px; + pointer-events: ${({ isInteractive }) => (isInteractive ? undefined : 'none')}; + width: 100%; +`; -const Chevron = styled.div( - css({ - ml: 3, - backgroundImage: 'url("/icons/chevron-black.svg")', - backgroundSize: '0.5em 0.9em', - backgroundPosition: '0 50%', - backgroundRepeat: 'no-repeat', - width: '0.5em', - height: '1em', - display: 'block', - }) -); +const StyledTooltipHeader = styled(Box)` + align-items: center; + display: flex; + justify-content: space-between; + padding: ${space[2]} ${space[3]}; + white-space: nowrap; +`; -const TooltipInfo = styled.div( - css({ - cursor: 'pointer', - borderTop: '1px solid', - borderTopColor: 'gray3', - padding: `${space[2]} ${space[3]}`, - }) -); +const StyledChevronRight = styled(ChevronRight)` + color: ${colors.black}; + height: ${space[3]}; +`; -const StyledLocationIcon = styled.span( - css({ - whiteSpace: 'nowrap', - display: 'inline-block', - mr: 2, +const StyledTooltipInfo = styled(Box)` + border-top: 1px solid ${colors.gray3}; + cursor: pointer; + padding: ${space[2]} ${space[3]}; +`; - svg: { - pt: '3px', - color: 'black', - width: 16, - height: 17, - }, - }) -); +const StyledLocationIcon = styled(InlineText)` + color: ${colors.black}; + margin-right: ${space[2]}; + white-space: nowrap; + + svg { + height: 18px; + padding-top: 3px; + width: 18px; + } +`; diff --git a/packages/app/src/components/collapsible/collapsible-section.tsx b/packages/app/src/components/collapsible/collapsible-section.tsx index a6afe2285f..64855f16a9 100644 --- a/packages/app/src/components/collapsible/collapsible-section.tsx +++ b/packages/app/src/components/collapsible/collapsible-section.tsx @@ -1,4 +1,3 @@ -import { css } from '@styled-system/css'; import { ReactNode, useEffect, useRef } from 'react'; import styled from 'styled-components'; import { Box, BoxProps } from '~/components/base'; @@ -7,6 +6,7 @@ import { isElementAtTopOfViewport } from '~/utils/is-element-at-top-of-viewport' import { useCollapsible } from '~/utils/use-collapsible'; import { Anchor } from '../typography'; import { colors } from '@corona-dashboard/common'; +import { fontSizes, fontWeights, space } from '~/style/theme'; interface CollapsibleSectionProps extends BoxProps { summary: string; @@ -51,9 +51,9 @@ export const CollapsibleSection = ({ summary, children, id, hideBorder, textColo }, [toggle, id]); return ( - - collapsible.toggle()}> - + + collapsible.toggle()}> + {summary} {id && ( event.stopPropagation()} href={`#${id}`}> @@ -62,60 +62,55 @@ export const CollapsibleSection = ({ summary, children, id, hideBorder, textColo )} {collapsible.button()} - - {collapsible.content({children})} + + {collapsible.content({children})} ); }; -const StyledAnchor = styled(Anchor)( - css({ - color: colors.gray2, - px: 3, - py: 1, - width: 0, - textDecoration: 'none', - position: 'absolute', - right: '100%', - '&:hover, &:focus': { - color: colors.blue1, - }, - }) -); +const StyledAnchor = styled(Anchor)` + color: ${colors.gray2}; + left: -48px; + padding: 0 ${space[3]}; + position: absolute; + text-decoration: none; + top: 50%; + transform: translateY(-50%); + width: 0; + + &:hover, + &:focus { + color: ${colors.blue1}; + } +`; interface SummaryProps { textColor: string; } -const Summary = styled.div((summaryProps: SummaryProps) => - css({ - display: 'flex', - alignItems: 'center', - justifyContent: 'space-between', - overflow: 'visible', - width: '100%', - margin: 0, - padding: 3, - bg: 'transparent', - border: 'none', - color: summaryProps.textColor, - fontFamily: 'body', - fontWeight: 'bold', - fontSize: '1.25rem', - textAlign: 'left', - position: 'relative', - cursor: 'pointer', - userSelect: 'none', - - '&:focus': { - outlineWidth: '1px', - outlineStyle: 'dashed', - outlineColor: colors.blue8, - }, - - [StyledAnchor]: { opacity: 0 }, - - '&:focus, &:hover': { - [StyledAnchor]: { opacity: 1 }, - }, - }) -); + +const StyledSummary = styled(Box)` + align-items: center; + color: ${({ textColor }) => textColor}; + cursor: pointer; + display: flex; + font-size: ${fontSizes[5]}; + font-weight: ${fontWeights.bold}; + justify-content: space-between; + padding: ${space[3]}; + user-select: none; + + &:focus { + outline: 1px dashed ${colors.blue8}; + } + + ${StyledAnchor} { + opacity: 0; + } + + &:hover, + &:focus { + ${StyledAnchor} { + opacity: 1; + } + } +`; diff --git a/packages/app/src/components/fullscreen-chart-tile.tsx b/packages/app/src/components/fullscreen-chart-tile.tsx index 62c668eb97..4c670e1359 100644 --- a/packages/app/src/components/fullscreen-chart-tile.tsx +++ b/packages/app/src/components/fullscreen-chart-tile.tsx @@ -1,8 +1,10 @@ +import { colors } from '@corona-dashboard/common'; import { Close, Expand } from '@corona-dashboard/icons'; -import css from '@styled-system/css'; import { useEffect, useRef, useState } from 'react'; +import styled from 'styled-components'; import { Tile } from '~/components/tile'; import { useIntl } from '~/intl'; +import { space } from '~/style/theme'; import { replaceVariablesInText } from '~/utils/replace-variables-in-text'; import { useBreakpoints } from '~/utils/use-breakpoints'; import { usePrevious } from '~/utils/use-previous'; @@ -12,15 +14,13 @@ import { IconButton } from './icon-button'; import { Metadata, MetadataProps } from './metadata'; import { Modal } from './modal'; -export function FullscreenChartTile({ - children, - metadata, - disabled, -}: { +interface FullscreenChartTileProps { children: React.ReactNode; metadata?: MetadataProps; disabled?: boolean; -}) { +} + +export function FullscreenChartTile({ children, metadata, disabled }: FullscreenChartTileProps) { const [isFullscreen, setIsFullscreen] = useState(false); const wasFullscreen = usePrevious(isFullscreen); const breakpoints = useBreakpoints(); @@ -33,18 +33,13 @@ export function FullscreenChartTile({ } }, [wasFullscreen, isFullscreen]); - const label = replaceVariablesInText( - isFullscreen - ? commonTexts.common.modal_close - : commonTexts.common.modal_open, - { subject: commonTexts.common.grafiek_singular } - ); + const label = replaceVariablesInText(isFullscreen ? commonTexts.common.modal_close : commonTexts.common.modal_open, { subject: commonTexts.common.grafiek_singular }); const tile = ( - + > )} {!disabled && breakpoints.md && ( - - setIsFullscreen((x) => !x)} - size={16} - > + + setIsFullscreen((previousValue) => !previousValue)} size={16}> {isFullscreen ? : } - + )} @@ -91,11 +66,7 @@ export function FullscreenChartTile({ if (!disabled && breakpoints.md && isFullscreen) { return ( - setIsFullscreen(false)} - isFullheight - > + setIsFullscreen(false)} isFullheight> {tile} ); @@ -103,3 +74,22 @@ export function FullscreenChartTile({ return {tile}; } + +interface StyledModalCloseButtonWrapperProps { + isFullscreen: boolean; +} + +const StyledModalCloseButtonWrapper = styled.div` + color: ${colors.gray3}; + position: absolute; + right: ${({ isFullscreen }) => (isFullscreen ? '25px' : '0')}; + top: 25px; + + &:focus { + outline: 1px dashed ${colors.blue8}; + } + + &:hover { + color: ${colors.gray5}; + } +`; diff --git a/packages/app/src/components/icon-button.tsx b/packages/app/src/components/icon-button.tsx index ee0f37a241..57db91e0e9 100644 --- a/packages/app/src/components/icon-button.tsx +++ b/packages/app/src/components/icon-button.tsx @@ -1,4 +1,3 @@ -import css from '@styled-system/css'; import { cloneElement, forwardRef, ReactElement } from 'react'; import styled from 'styled-components'; import { VisuallyHidden } from './visually-hidden'; @@ -12,54 +11,33 @@ interface IconButtonProps { padding?: number | string; } -export const IconButton = forwardRef( - ( - { - children, - size, - title, - color = 'currentColor', - onClick, - padding, - ...ariaProps - }, - ref - ) => { - return ( - - {title} - {cloneElement(children, { 'aria-hidden': "true" })} - - ); - } -); +export const IconButton = forwardRef(({ children, size, title, color = 'currentColor', onClick, padding, ...ariaProps }, ref) => { + return ( + + {title} + {cloneElement(children, { 'aria-hidden': 'true' })} + + ); +}); -const StyledIconButton = styled.button<{ +interface StyledIconButtonProps { color: string; size: number; padding?: number | string; -}>((x) => - css({ - p: x.padding ?? 0, - m: 0, - bg: 'transparent', - border: 'none', - display: 'block', - cursor: 'pointer', - color: x.color, - '& svg': { - display: 'block', - width: x.size, - height: x.size, - }, - }) -); +} + +const StyledIconButton = styled.button` + background: transparent; + border: none; + color: ${({ color }) => color}; + cursor: pointer; + display: block; + margin: 0; + padding: ${({ padding }) => (padding ? `${padding}px` : 0)}; + + & svg { + display: block; + height: ${({ size }) => size}px; + width: ${({ size }) => size}px; + } +`; diff --git a/packages/app/src/components/page-information-block/page-information-block.tsx b/packages/app/src/components/page-information-block/page-information-block.tsx index c5537d7142..7e76b12706 100644 --- a/packages/app/src/components/page-information-block/page-information-block.tsx +++ b/packages/app/src/components/page-information-block/page-information-block.tsx @@ -16,6 +16,8 @@ import { PageLinks } from './components/page-links'; import { WarningTile } from '~/components/warning-tile'; import { useScopedWarning } from '~/utils/use-scoped-warning'; import { useIntl } from '~/intl'; +import { colors } from '@corona-dashboard/common'; +import { space } from '~/style/theme'; interface InformationBlockProps { title?: string; @@ -57,18 +59,12 @@ export function PageInformationBlock({ onToggleArchived, }: InformationBlockProps) { const scopedWarning = useScopedWarning(vrNameOrGmName || '', warning || ''); - const showArchivedToggleButton = - typeof isArchivedHidden !== 'undefined' && - typeof onToggleArchived !== 'undefined'; + const showArchivedToggleButton = typeof isArchivedHidden !== 'undefined' && typeof onToggleArchived !== 'undefined'; const { commonTexts } = useIntl(); const MetaDataBlock = metadata ? ( - + ) : null; @@ -86,22 +82,8 @@ export function PageInformationBlock({ return ( - {title && ( - - )} - {scopedWarning && ( - - )} + {title && } + {scopedWarning && } {description && ( @@ -139,15 +121,9 @@ export function PageInformationBlock({ {showArchivedToggleButton && ( - - {!isArchivedHidden - ? commonTexts.common.show_archived - : commonTexts.common.hide_archived} - + + {!isArchivedHidden ? commonTexts.common.show_archived : commonTexts.common.hide_archived} + )} @@ -172,14 +148,28 @@ const MetadataBox = styled.div( }) ); -const Button = styled.button<{ isActive?: boolean }>(({ isActive }) => - css({ - bg: !isActive ? 'blue8' : 'transparent', - border: 'none', - borderRadius: '5px', - color: !isActive ? 'white' : 'blue8', - px: !isActive ? 3 : 0, - py: !isActive ? 12 : 0, - cursor: 'pointer', - }) -); +interface StyledArchiveButtonProps { + isActive?: boolean; +} + +const StyledArchiveButton = styled.button` + background: ${({ isActive }) => (isActive ? colors.blue1 : colors.white)}; + border: ${({ isActive }) => (isActive ? colors.transparent : colors.gray3)}; + border-radius: 5px; + border-style: solid; + border-width: 1px; + color: ${({ isActive }) => (isActive ? colors.blue8 : colors.blue8)}; + cursor: pointer; + min-height: 36px; + padding: 12px ${space[3]}; + + &:hover { + background: ${colors.blue8}; + color: ${colors.white}; + border-color: ${colors.transparent}; + } + + &:hover:focus-visible { + outline: 2px dotted ${colors.magenta3}; + } +`; diff --git a/packages/app/src/components/time-series-chart/components/tooltip/tooltip-wrapper.tsx b/packages/app/src/components/time-series-chart/components/tooltip/tooltip-wrapper.tsx index a91e318a2e..9542ae5f08 100644 --- a/packages/app/src/components/time-series-chart/components/tooltip/tooltip-wrapper.tsx +++ b/packages/app/src/components/time-series-chart/components/tooltip/tooltip-wrapper.tsx @@ -1,7 +1,7 @@ import { colors } from '@corona-dashboard/common'; -import css from '@styled-system/css'; import { ReactNode } from 'react'; import styled from 'styled-components'; +import { space, fontSizes, shadows } from '~/style/theme'; import { isDefined } from 'ts-is-present'; import { BoldText } from '~/components/typography'; import { useBoundingBox } from '~/utils/use-bounding-box'; @@ -28,14 +28,7 @@ const VIEWPORT_PADDING = 10; * * @TODO clean up calculations in Tooltip component */ -export function TooltipWrapper({ - title, - children, - left, - top: _top, - bounds, - padding, -}: TooltipWrapperProps) { +export function TooltipWrapper({ title, children, left, top: _top, bounds, padding }: TooltipWrapperProps) { const viewportSize = useViewport(); const isMounted = useIsMounted({ delayMs: 10 }); const [ref, { width = 0, height = 0 }] = useResizeObserver(); @@ -44,10 +37,7 @@ export function TooltipWrapper({ const targetY = -height; const targetX = left + padding.left; - const maxWidth = Math.min( - bounds.width + padding.left + padding.right, - viewportSize.width - VIEWPORT_PADDING * 2 - ); + const maxWidth = Math.min(bounds.width + padding.left + padding.right, viewportSize.width - VIEWPORT_PADDING * 2); const relativeLeft = boundingBox?.left ?? 0; @@ -66,10 +56,10 @@ export function TooltipWrapper({ return ( <> - {children} - + > ); } +interface StyledTooltipContainerProps { + isMounted: boolean; +} + +const StyledTooltipContainer = styled.div` + background: ${colors.white}; + border-radius: 1px; + box-shadow: ${shadows.tooltip}; + opacity: ${(props) => (props.isMounted ? 1 : 0)}; + pointer-events: none; + position: absolute; + top: 0; + will-change: transform; + z-index: 1000; +`; + interface TriangleProps { + isMounted: boolean; left: number; top: number; - isMounted: boolean; } function Triangle({ left, top, isMounted }: TriangleProps) { return ( - - + ); } -const StyledTriangle = styled.div<{ width: number }>((x) => { - /** - * 🙏 pythagoras - */ - const borderWidth = Math.sqrt(Math.pow(x.width, 2) / 2) / 2; - - return css({ - position: 'relative', - width: 0, - height: 0, - marginLeft: -borderWidth, - boxSizing: 'border-box', - borderWidth, - borderStyle: 'solid', - borderColor: 'transparent transparent white white', - transformOrigin: '0 0', - transform: 'rotate(-45deg)', - boxShadow: `-3px 3px 3px 0 ${colors.blackOpacity}`, - }); -}); - -const TooltipContainer = styled.div( - css({ - position: 'absolute', - bg: 'white', - boxShadow: 'tooltip', - pointerEvents: 'none', - zIndex: 1000, - borderRadius: 1, - top: 0, - willChange: 'transform', - }) -); +interface StyledTriangleWrapperProps { + isMounted: boolean; +} + +const StyledTriangleWrapper = styled.div` + left: 0; + opacity: ${(props) => (props.isMounted ? 1 : 0)}; + pointer-events: none; + position: absolute; + top: 0; + z-index: 1010; +`; + +interface StyledTriangleProps { + width: number; +} + +const calcPythagoras = (width: number) => Math.sqrt(Math.pow(width, 2) / 2) / 2; + +const StyledTriangle = styled.div` + border-color: ${colors.transparent} ${colors.transparent} ${colors.white} ${colors.white}; + border-style: solid; + border-width: ${(props) => calcPythagoras(props.width)}px; + box-shadow: -3px 3px 3px 0 ${colors.blackOpacity}; + box-sizing: border-box; + height: 0; + margin-left: -${(props) => calcPythagoras(props.width)}px; + position: relative; + transform-origin: 0 0; + transform: rotate(-45deg); + width: 0; +`; interface TooltipContentProps { - title?: string; - onSelect?: (event: React.MouseEvent) => void; children?: ReactNode; + onSelect?: (event: React.MouseEvent) => void; + title?: string; } function TooltipContent(props: TooltipContentProps) { @@ -159,50 +157,52 @@ function TooltipContent(props: TooltipContentProps) { return ( {title && } - {children && ( - - {children} - - )} + {children && {children}} ); } function TooltipHeading({ title }: { title: string }) { return ( - + {title} - + ); } -const TooltipChildren = styled.div<{ hasTitle?: boolean }>(({ hasTitle }) => - css({ - borderTop: hasTitle ? '1px solid' : '', - borderTopColor: hasTitle ? 'gray3' : '', - py: 2, - px: 3, - }) -); - -const StyledTooltipContent = styled.div((x) => - css({ - color: 'black', - maxWidth: 425, - borderRadius: 1, - cursor: x.onClick ? 'pointer' : 'default', - fontSize: 1, - }) -); +interface StyledTooltipContentProps { + onClick?: (event: React.MouseEvent) => void; +} + +const StyledTooltipContent = styled.div` + border-radius: 1px; + color: ${colors.black}; + cursor: ${(props) => (props.onClick ? 'pointer' : 'default')}; + font-size: ${fontSizes[1]}; + max-width: 440px; +`; + +interface StyledTooltipHeadingWrapperProps { + title?: string; +} + +const StyledTooltipHeadingWrapper = styled.div` + align-items: center; + color: ${colors.black}; + display: flex; + justify-content: space-between; + overflow: hidden; + padding: ${space[2]} ${space[3]}; + text-overflow: ellipsis; + white-space: nowrap; +`; + +interface StyledTooltipChildrenProps { + hasTitle?: boolean; +} + +const StyledTooltipChildren = styled.div` + border-top: ${(props) => (props.hasTitle ? '1px solid' : '')}; + border-top-color: ${(props) => (props.hasTitle ? colors.gray3 : '')}; + padding: ${space[2]} ${space[3]}; +`; diff --git a/packages/app/src/components/trend-icon.tsx b/packages/app/src/components/trend-icon.tsx index 27d430de4b..6cb7051455 100644 --- a/packages/app/src/components/trend-icon.tsx +++ b/packages/app/src/components/trend-icon.tsx @@ -1,33 +1,82 @@ +import { colors } from '@corona-dashboard/common'; +import { Down, Dot, Up, ArrowWithIntensity } from '@corona-dashboard/icons'; +import styled from 'styled-components'; import { useIntl } from '~/intl'; -import { Down, Dot, Up } from '@corona-dashboard/icons'; +import { space } from '~/style/theme'; export enum TrendDirection { UP, DOWN, NEUTRAL, } + interface TrendIconProps { trendDirection: TrendDirection; + intensity?: number | null; + color?: string | null; ariaLabel?: string; } -export const TrendIcon = ({ trendDirection, ariaLabel }: TrendIconProps) => { +export const TrendIcon = ({ trendDirection, ariaLabel, intensity = null, color = null }: TrendIconProps) => { const { commonTexts } = useIntl(); - const TrendLabelUp = ariaLabel || commonTexts.accessibility.visual_context_labels.up_trend_label; - const TrendLabelDown = ariaLabel || commonTexts.accessibility.visual_context_labels.down_trend_label; - const TrendLabelNeutral = ariaLabel || commonTexts.accessibility.visual_context_labels.neutral_trend_label; + const trendLabels: { [key: string]: string } = { + up: ariaLabel || commonTexts.accessibility.visual_context_labels.up_trend_label, + down: ariaLabel || commonTexts.accessibility.visual_context_labels.down_trend_label, + neutral: ariaLabel || commonTexts.accessibility.visual_context_labels.neutral_trend_label, + }; + + const ariaLabelText = trendLabels[TrendDirection[trendDirection].toLowerCase()]; + + // Icon with intensity is used only on the homepage at the moment, for all other trend icons the default (below) are used. + if (intensity && color && TrendDirection[trendDirection]) { + return ; + } switch (trendDirection) { case TrendDirection.UP: - return ; + return ; case TrendDirection.DOWN: - return ; + return ; case TrendDirection.NEUTRAL: - return ; + return ; default: { const exhaustiveCheck: never = trendDirection; throw new Error(`Unhandled TrendDirection case: ${exhaustiveCheck}`); } } }; + +const intensitySelectors: { [key: number]: { fill: string; stroke?: string } } = { + 1: { + fill: '.one-arrow', + stroke: '.two-stroke, .three-stroke', + }, + 2: { + fill: '.one-arrow, .two-arrows', + stroke: '.three-stroke', + }, + 3: { + fill: '.one-arrow, .two-arrows, .three-arrows', + }, +}; + +interface TrendIconWithIntensityProps { + color: string; + direction: TrendDirection; + intensity: number; +} + +const TrendIconWithIntensity = styled(ArrowWithIntensity)` + flex-shrink: 0; + margin-left: ${space[2]}; + transform: ${({ direction }) => (direction === TrendDirection.DOWN ? 'scaleY(-1)' : undefined)}; + + ${({ intensity, color }): string => + `${intensitySelectors[intensity].fill} { + fill: ${color}; + } + ${intensitySelectors[intensity].stroke} { + stroke: ${colors.gray7}; + }`} +`; diff --git a/packages/app/src/components/typography.tsx b/packages/app/src/components/typography.tsx index bff978e848..300b21d227 100644 --- a/packages/app/src/components/typography.tsx +++ b/packages/app/src/components/typography.tsx @@ -1,4 +1,4 @@ -import { Color } from '@corona-dashboard/common'; +import { Color, colors } from '@corona-dashboard/common'; import css, { CSSProperties } from '@styled-system/css'; import styled, { DefaultTheme } from 'styled-components'; import { Preset, preset } from '~/style/preset'; @@ -13,20 +13,7 @@ export interface TextProps { ariaLabel?: string; } -export interface AnchorProps extends TextProps { - underline?: boolean | 'hover'; - hoverColor?: TextProps['color']; - display?: string; - width?: string | number; -} - -export interface HeadingProps extends TextProps { - level: HeadingLevel; -} - -export type HeadingLevel = 1 | 2 | 3 | 4 | 5; - -function textStyle(props: TextProps & { as?: string }) { +export const textStyle = (props: TextProps & { as?: string }) => { return css({ ...(props.as === 'button' ? { @@ -40,16 +27,13 @@ function textStyle(props: TextProps & { as?: string }) { : undefined), ...(props.variant ? preset.typography[props.variant] : undefined), - ...(props.fontWeight ? { fontWeight: props.fontWeight } : undefined), ...(props.color ? { color: props.color } : undefined), - ...(props.textTransform - ? { textTransform: props.textTransform } - : undefined), + ...(props.textTransform ? { textTransform: props.textTransform } : undefined), ...(props.textAlign ? { textAlign: props.textAlign } : undefined), ...(props.hyphens ? { hyphens: props.hyphens } : undefined), }); -} +}; export const Text = styled.p(textStyle); @@ -57,40 +41,62 @@ export const InlineText = styled.span(textStyle); export const BoldText = styled.strong(textStyle); -export const Anchor = styled.a( - textStyle, - (props) => - props.underline && - css({ - textDecoration: props.underline === 'hover' ? 'none' : 'underline', - '&:hover, &:focus': { - span: { - textDecoration: 'underline', - }, - }, - }), - (props) => - props.hoverColor && - css({ - '&:hover,&:focus': { color: 'blue8' }, - }), - (props) => - props.display && - css({ - display: props.display, - }) -); +export interface AnchorProps extends TextProps { + underline?: boolean | 'hover'; + hoverColor?: TextProps['color']; + display?: string; + width?: string | number; +} + +export const Anchor = styled.a` + ${textStyle} + ${({ underline }) => + underline + ? ` + cursor: pointer; + text-decoration: ${underline === 'hover' ? 'none' : 'underline'}; + + &:hover, + &:focus { + text-decoration: underline; -export const Heading = styled.h1.attrs( - (props: HeadingProps & { as?: string }) => ({ - as: props.as ?? (`h${props.level}` as const), - variant: props.variant ?? (`h${props.level}` as const), - }) -)(textStyle); + span { + text-decoration: underline; + } + } + ` + : undefined} + ${({ hoverColor }) => + hoverColor + ? ` + &:hover, + &:focus { + color: ${colors.blue8}; + } + ` + : undefined} + ${({ display }) => + display + ? ` + display: ${display}; + ` + : undefined} +`; -export function styledTextVariant(variant: string, as?: string) { +export interface HeadingProps extends TextProps { + level: HeadingLevel; +} + +export type HeadingLevel = 1 | 2 | 3 | 4 | 5; + +export const Heading = styled.h1.attrs((props: HeadingProps & { as?: string }) => ({ + as: props.as ?? (`h${props.level}` as const), + variant: props.variant ?? (`h${props.level}` as const), +}))(textStyle); + +export const styledTextVariant = (variant: string, as?: string) => { return styled.p.attrs(() => ({ as: as ?? 'p', variant, })); -} +}; diff --git a/packages/app/src/domain/layout/gm-layout.tsx b/packages/app/src/domain/layout/gm-layout.tsx index 22c976673a..b36b42da99 100644 --- a/packages/app/src/domain/layout/gm-layout.tsx +++ b/packages/app/src/domain/layout/gm-layout.tsx @@ -4,9 +4,10 @@ import { Menu, MenuRenderer } from '~/components/aside/menu'; import { Box } from '~/components/base'; import { ErrorBoundary } from '~/components/error-boundary'; import { AppContent } from '~/components/layout/app-content'; -import { Heading, Text } from '~/components/typography'; +import { Anchor, Heading, Text } from '~/components/typography'; import { VisuallyHidden } from '~/components/visually-hidden'; import { useIntl } from '~/intl'; +import { space } from '~/style/theme'; import { getVrForMunicipalityCode } from '~/utils/get-vr-for-municipality-code'; import { Link } from '~/utils/link'; import { useReverseRouter } from '~/utils/use-reverse-router'; @@ -64,10 +65,7 @@ export function GmLayout(props: GmLayoutProps) { layout: 'gm', code: code, map: [ - [ - 'development_of_the_virus', - ['sewage_measurement', 'positive_tests', 'mortality'], - ], + ['development_of_the_virus', ['sewage_measurement', 'positive_tests', 'mortality']], ['consequences_for_healthcare', ['hospital_admissions']], ['actions_to_take', ['vaccinations']], ], @@ -76,23 +74,14 @@ export function GmLayout(props: GmLayoutProps) { return ( <> - - + + + } @@ -107,21 +96,19 @@ export function GmLayout(props: GmLayoutProps) { aria-labelledby="sidebar-title" role="navigation" maxWidth={{ _: '38rem', md: undefined }} - mx="auto" + marginX="auto" spacing={1} > - + - - {commonTexts.gemeente_layout.headings.sidebar} - + {commonTexts.gemeente_layout.headings.sidebar} {municipalityName} {vr && ( {commonTexts.common.veiligheidsregio_label}{' '} - {vr.name} + {vr.name} )} diff --git a/packages/app/src/domain/topical/components/topical-measure-tile.tsx b/packages/app/src/domain/topical/components/topical-measure-tile.tsx index 2e6ec5dabe..487884cd82 100644 --- a/packages/app/src/domain/topical/components/topical-measure-tile.tsx +++ b/packages/app/src/domain/topical/components/topical-measure-tile.tsx @@ -14,11 +14,11 @@ interface TopicalMeasureTileProps { export const TopicalMeasureTile = ({ icon, title }: TopicalMeasureTileProps) => { return ( - + - + - + @@ -28,7 +28,7 @@ export const TopicalMeasureTile = ({ icon, title }: TopicalMeasureTileProps) => ); }; -const KpiIcon = styled.div` +const StyledKpiIcon = styled.div` color: ${colors.blue8}; display: flex; width: 40px; diff --git a/packages/app/src/domain/topical/components/topical-theme-header.tsx b/packages/app/src/domain/topical/components/topical-theme-header.tsx index b26cd806d5..c5a1f01e33 100644 --- a/packages/app/src/domain/topical/components/topical-theme-header.tsx +++ b/packages/app/src/domain/topical/components/topical-theme-header.tsx @@ -18,9 +18,9 @@ export const TopicalThemeHeader = ({ title, subtitle, icon }: TopicalThemeHeader return ( - + - + {title} {subtitle && ( @@ -32,11 +32,11 @@ export const TopicalThemeHeader = ({ title, subtitle, icon }: TopicalThemeHeader ); }; -const TopicalThemeHeaderIcon = styled.span` +const StyledTopicalThemeHeaderIcon = styled.span` display: block; height: 25px; margin-right: 10px; - width: 25px; + min-width: 25px; @media ${theme.mediaQueries.sm} { height: 30px; diff --git a/packages/app/src/domain/topical/components/topical-tile.tsx b/packages/app/src/domain/topical/components/topical-tile.tsx index 8a1859a384..8dce26d17f 100644 --- a/packages/app/src/domain/topical/components/topical-tile.tsx +++ b/packages/app/src/domain/topical/components/topical-tile.tsx @@ -84,18 +84,14 @@ export function TopicalTile({ title, tileIcon, trendIcon, description, kpiValue, > {title} {!formattedKpiValue && trendIcon.direction && trendIcon.color && ( - - - + )} {formattedKpiValue && ( {trendIcon.direction && trendIcon.color && ( - - - + )} )} @@ -135,13 +131,6 @@ export function TopicalTile({ title, tileIcon, trendIcon, description, kpiValue, ); } -const TrendIconWrapper = styled.span` - color: ${({ color }) => color}; - flex-shrink: 0; - margin-left: ${space[2]}; - width: 20px; -`; - const TileIcon = styled.span` background-color: ${colors.blue8}; border-bottom-left-radius: ${space[1]}; diff --git a/packages/app/src/domain/topical/types.ts b/packages/app/src/domain/topical/types.ts index 2d8dbf2d6e..ca1f7cbd62 100644 --- a/packages/app/src/domain/topical/types.ts +++ b/packages/app/src/domain/topical/types.ts @@ -6,4 +6,5 @@ export type TrendIconDirection = typeof ICON_DIRECTION_UP | typeof ICON_DIRECTIO export type TrendIcon = { direction: TrendIconDirection | null; color: TrendIconColor | null; + intensity: 1 | 2 | 3 | null; }; diff --git a/packages/app/src/pages/landelijk/gedrag.tsx b/packages/app/src/pages/landelijk/gedrag.tsx index caf669b4c2..bfc58de97c 100644 --- a/packages/app/src/pages/landelijk/gedrag.tsx +++ b/packages/app/src/pages/landelijk/gedrag.tsx @@ -1,5 +1,6 @@ import { Bevolking } from '@corona-dashboard/icons'; import { GetStaticPropsContext } from 'next'; +import { middleOfDayInSeconds } from '@corona-dashboard/common'; import { useMemo, useRef, useState } from 'react'; import { Box } from '~/components/base'; import { Markdown } from '~/components/markdown'; @@ -99,8 +100,8 @@ export default function BehaviorPage(props: StaticProps) .map((event) => ({ title: event[`message_title_${locale}`], description: event[`message_desc_${locale}`], - start: event.date_start_unix, - end: event.date_end_unix, + start: middleOfDayInSeconds(event.date_start_unix), + end: middleOfDayInSeconds(event.date_end_unix), })); return { currentTimelineEvents }; diff --git a/packages/app/src/pages/landelijk/geldende-adviezen.tsx b/packages/app/src/pages/landelijk/geldende-adviezen.tsx index bcb3947eaf..af9688265e 100644 --- a/packages/app/src/pages/landelijk/geldende-adviezen.tsx +++ b/packages/app/src/pages/landelijk/geldende-adviezen.tsx @@ -41,7 +41,8 @@ export const getStaticProps = createGetStaticProps( } }, 'title':title.${locale}, - 'description': description.${locale} + 'description': description.${locale}, + 'collectionTitle': collectionTitle.${locale} }, }`; }) @@ -70,7 +71,7 @@ const NationalRestrictions = (props: StaticProps) => { )} - {measures.title} + {measures.collectionTitle} diff --git a/packages/app/src/queries/get-topical-structure-query.ts b/packages/app/src/queries/get-topical-structure-query.ts index 0249248a93..4667bd0d37 100644 --- a/packages/app/src/queries/get-topical-structure-query.ts +++ b/packages/app/src/queries/get-topical-structure-query.ts @@ -5,13 +5,13 @@ export function getTopicalStructureQuery(locale: string) { const query = `// groq { 'topicalConfig': *[ - _type == 'topicalPageConfig' && !(_id in path("drafts.**")) + _type == 'topicalPageConfig' && !(_id in path('drafts.**')) ][0]{ 'title': title.${locale}, 'description': description.${locale} }, 'weeklySummary': *[ - _type == 'weeklySummary' && !(_id in path("drafts.**")) + _type == 'weeklySummary' && !(_id in path('drafts.**')) ][0]{ 'title': title.${locale}, 'items': items[]->{ @@ -21,30 +21,31 @@ export function getTopicalStructureQuery(locale: string) { }, }, 'kpiThemes': *[ - _type == 'themeCollection' && !(_id in path("drafts.**")) + _type == 'themeCollection' && !(_id in path('drafts.**')) ][0]{ 'themes': themes[]->{ - "title":title.${locale}, - "subTitle":subTitle.${locale}, + 'title':title.${locale}, + 'subTitle':subTitle.${locale}, themeIcon, 'linksLabelMobile': labelMobile.${locale}, 'linksLabelDesktop': labelDesktop.${locale}, - "links":links[]->{ + 'links':links[]->{ 'cta': { 'title': cta.title.${locale}, 'href': cta.href }, }, - "tiles":tiles[]->{ - "description":description.${locale}, + 'tiles':tiles[]->{ + 'description':description.${locale}, tileIcon, - "title":title.${locale}, - "sourceLabel":sourceLabel.${locale}, + 'title':title.${locale}, + 'sourceLabel':sourceLabel.${locale}, 'kpiValue': kpiValue.${locale}, 'trendIcon': { 'color': trendIcon.color, 'direction': trendIcon.direction, + 'intensity': trendIcon.intensity, }, 'cta': { 'title': cta.title.${locale}, @@ -54,7 +55,7 @@ export function getTopicalStructureQuery(locale: string) { }, }, 'measureTheme': *[ - _type == 'measureTheme' && !(_id in path("drafts.**")) + _type == 'measureTheme' && !(_id in path('drafts.**')) ][0]{ 'title': title.${locale}, themeIcon, @@ -65,12 +66,12 @@ export function getTopicalStructureQuery(locale: string) { }, }, 'thermometer': *[ - _type == 'thermometer' && !(_id in path("drafts.**")) + _type == 'thermometer' && !(_id in path('drafts.**')) ][0]{ icon, 'title': title.${locale}, 'subTitle': subTitle.${locale}, - "tileTitle":tileTitle.${locale}, + 'tileTitle':tileTitle.${locale}, currentLevel, 'thermometerLevels': thermometerLevels[]->{ 'level': level, diff --git a/packages/app/src/style/theme.ts b/packages/app/src/style/theme.ts index 6a8738915b..99811ab972 100644 --- a/packages/app/src/style/theme.ts +++ b/packages/app/src/style/theme.ts @@ -89,7 +89,7 @@ const mediaQueries = { const radii = [0, 5, 10]; -const shadows = { +export const shadows = { tile: '0px 4px 8px rgba(0, 0, 0, 0.1)', tooltip: '0px 2px 12px rgba(0, 0, 0, 0.1)', } as const; diff --git a/packages/app/src/types/cms.d.ts b/packages/app/src/types/cms.d.ts index 5f4fd430b0..ad8392e166 100644 --- a/packages/app/src/types/cms.d.ts +++ b/packages/app/src/types/cms.d.ts @@ -174,6 +174,7 @@ export type Measures = { icon: string; title: string; description: RichContentBlock[] | null; + collectionTitle: string; measuresCollection: MeasuresCollection[]; }; declare module 'picosanity' { diff --git a/packages/cms/src/data/data-structure.ts b/packages/cms/src/data/data-structure.ts index 2070f7d078..cc4603fdd1 100644 --- a/packages/cms/src/data/data-structure.ts +++ b/packages/cms/src/data/data-structure.ts @@ -12,16 +12,18 @@ export const dataStructure = { 'admissions_on_date_of_reporting', ], tested_overall: ['infected', 'infected_moving_average', 'infected_moving_average_rounded', 'infected_per_100k', 'infected_per_100k_moving_average'], - sewer: ['average', 'total_number_of_samples', 'sampled_installation_count', 'total_installation_count'], + sewer: ['average', 'total_number_of_samples', 'sampled_installation_count', 'total_installation_count', 'data_is_outdated'], vaccine_coverage_per_age_group: [ - 'age_group_range', - 'autumn_2022_vaccinated_percentage', - 'fully_vaccinated_percentage', - 'booster_shot_percentage', - 'birthyear_range', - 'autumn_2022_vaccinated_percentage_label', - 'fully_vaccinated_percentage_label', - 'booster_shot_percentage_label', + 'vaccination_type', + 'birthyear_range_12_plus', + 'birthyear_range_18_plus', + 'birthyear_range_60_plus', + 'vaccinated_percentage_12_plus', + 'vaccinated_percentage_12_plus_label', + 'vaccinated_percentage_18_plus', + 'vaccinated_percentage_18_plus_label', + 'vaccinated_percentage_60_plus', + 'vaccinated_percentage_60_plus_label', ], vaccine_coverage_per_age_group_archived: [ 'age_group_range', @@ -41,28 +43,30 @@ export const dataStructure = { 'booster_shot_percentage_label', 'has_one_shot_percentage_label', ], - booster_coverage: ['age_group', 'percentage', 'percentage_label'], + booster_coverage_archived_20220904: ['age_group', 'percentage', 'percentage_label'], }, gm_collection: { hospital_nice: ['admissions_on_date_of_admission', 'admissions_on_date_of_admission_per_100000', 'admissions_on_date_of_reporting'], hospital_nice_choropleth: ['admissions_on_date_of_admission', 'admissions_on_date_of_admission_per_100000', 'admissions_on_date_of_reporting'], tested_overall: ['infected_per_100k', 'infected'], - sewer: ['average', 'total_installation_count'], + sewer: ['average', 'total_installation_count', 'data_is_outdated'], vaccine_coverage_per_age_group: [ - 'age_group_range', - 'autumn_2022_vaccinated_percentage', - 'fully_vaccinated_percentage', - 'booster_shot_percentage', - 'birthyear_range', - 'autumn_2022_vaccinated_percentage_label', - 'fully_vaccinated_percentage_label', - 'booster_shot_percentage_label', + 'vaccination_type', + 'birthyear_range_12_plus', + 'birthyear_range_18_plus', + 'birthyear_range_60_plus', + 'vaccinated_percentage_12_plus', + 'vaccinated_percentage_12_plus_label', + 'vaccinated_percentage_18_plus', + 'vaccinated_percentage_18_plus_label', + 'vaccinated_percentage_60_plus', + 'vaccinated_percentage_60_plus_label', ], }, nl: { - booster_shot_administered: ['administered_total', 'ggd_administered_total', 'others_administered_total'], + booster_shot_administered_archived_20220904: ['administered_total', 'ggd_administered_total', 'others_administered_total'], repeating_shot_administered: ['ggd_administered_total'], - booster_coverage: ['age_group', 'percentage'], + booster_coverage_archived_20220904: ['age_group', 'percentage'], doctor: ['covid_symptoms_per_100k', 'covid_symptoms'], g_number: ['g_number'], infectious_people: ['margin_low', 'estimate', 'margin_high'], @@ -232,10 +236,8 @@ export const dataStructure = { 'age_group_total', 'autumn_2022_vaccinated', 'fully_vaccinated', - 'booster_shot', 'autumn_2022_vaccinated_percentage', 'fully_vaccinated_percentage', - 'booster_shot_percentage', 'date_of_report_unix', 'birthyear_range', ], @@ -294,7 +296,7 @@ export const dataStructure = { topical: {}, vr: { g_number: ['g_number'], - sewer: ['average'], + sewer: ['average', 'data_is_outdated'], tested_overall: ['infected', 'infected_moving_average', 'infected_moving_average_rounded', 'infected_per_100k', 'infected_per_100k_moving_average'], hospital_nice: [ 'admissions_on_date_of_admission', @@ -397,14 +399,16 @@ export const dataStructure = { 'other', ], vaccine_coverage_per_age_group: [ - 'age_group_range', - 'autumn_2022_vaccinated_percentage', - 'fully_vaccinated_percentage', - 'booster_shot_percentage', - 'birthyear_range', - 'autumn_2022_vaccinated_percentage_label', - 'fully_vaccinated_percentage_label', - 'booster_shot_percentage_label', + 'vaccination_type', + 'birthyear_range_12_plus', + 'birthyear_range_18_plus', + 'birthyear_range_60_plus', + 'vaccinated_percentage_12_plus', + 'vaccinated_percentage_12_plus_label', + 'vaccinated_percentage_18_plus', + 'vaccinated_percentage_18_plus_label', + 'vaccinated_percentage_60_plus', + 'vaccinated_percentage_60_plus_label', ], vaccine_coverage_per_age_group_archived: [ 'age_group_range', @@ -424,14 +428,14 @@ export const dataStructure = { 'booster_shot_percentage_label', 'has_one_shot_percentage_label', ], - booster_coverage: ['age_group', 'percentage', 'percentage_label'], + booster_coverage_archived_20220904: ['age_group', 'percentage', 'percentage_label'], }, vr_collection: { hospital_nice: ['admissions_on_date_of_admission', 'admissions_on_date_of_admission_per_100000', 'admissions_on_date_of_reporting'], hospital_nice_choropleth: ['admissions_on_date_of_admission', 'admissions_on_date_of_admission_per_100000', 'admissions_on_date_of_reporting'], tested_overall: ['infected_per_100k', 'infected'], nursing_home: ['newly_infected_people', 'newly_infected_locations', 'infected_locations_total', 'infected_locations_percentage', 'deceased_daily'], - sewer: ['average'], + sewer: ['average', 'data_is_outdated'], behavior_archived_20221019: [ 'number_of_participants', 'curfew_compliance', @@ -475,14 +479,16 @@ export const dataStructure = { elderly_at_home: ['positive_tested_daily', 'positive_tested_daily_per_100k', 'deceased_daily'], situations: ['has_sufficient_data', 'home_and_visits', 'work', 'school_and_day_care', 'health_care', 'gathering', 'travel', 'hospitality', 'other'], vaccine_coverage_per_age_group: [ - 'age_group_range', - 'autumn_2022_vaccinated_percentage', - 'fully_vaccinated_percentage', - 'booster_shot_percentage', - 'birthyear_range', - 'autumn_2022_vaccinated_percentage_label', - 'fully_vaccinated_percentage_label', - 'booster_shot_percentage_label', + 'vaccination_type', + 'birthyear_range_12_plus', + 'birthyear_range_18_plus', + 'birthyear_range_60_plus', + 'vaccinated_percentage_12_plus', + 'vaccinated_percentage_12_plus_label', + 'vaccinated_percentage_18_plus', + 'vaccinated_percentage_18_plus_label', + 'vaccinated_percentage_60_plus', + 'vaccinated_percentage_60_plus_label', ], }, }; diff --git a/packages/cms/src/lokalize/key-mutations.csv b/packages/cms/src/lokalize/key-mutations.csv index 29d273cf79..a7c6419a8a 100644 --- a/packages/cms/src/lokalize/key-mutations.csv +++ b/packages/cms/src/lokalize/key-mutations.csv @@ -1,17 +1 @@ timestamp,action,key,document_id,move_to -2022-11-07T15:09:11.939Z,add,common.choropleth_tooltip.gm.outdated_data_notification,TrUIlGMFRR6fBR627mmYKT,__ -2022-11-07T15:41:35.054Z,delete,common.choropleth_tooltip.gm.outdated_data_notification,TrUIlGMFRR6fBR627mmYKT,__ -2022-11-07T15:57:14.611Z,add,common.choropleth_tooltip.outdated_data_notification,Fgb5SAAswLDPRQ9KRzHRc8,__ -2022-11-07T16:00:34.118Z,delete,common.choropleth_tooltip.outdated_data_notification,Fgb5SAAswLDPRQ9KRzHRc8,__ -2022-11-07T16:01:10.105Z,add,common.choropleth_tooltip.gm.average.outdated_data_notification,Fgb5SAAswLDPRQ9KRzIAnG,__ -2022-11-07T16:01:11.103Z,add,common.choropleth_tooltip.vr.average.outdated_data_notification,BBCOmHCrMQzaQKV1wWKvZ9,__ -2022-11-11T08:49:48.826Z,add,pages.sewer_page.nl.choropleth_legend_outdated_data_label,TrUIlGMFRR6fBR628D5VMd,__ -2022-11-21T09:11:46.413Z,add,common.common.age_groups.12,UE7TbOA80ovT85nyy1gcYy,__ -2022-11-21T09:11:47.332Z,add,common.common.age_groups.18,UE7TbOA80ovT85nyy1gcqO,__ -2022-11-21T09:11:48.400Z,add,common.common.age_groups.60,Tg1qSQfR7l8JBUWv7hoA0J,__ -2022-11-21T09:11:49.412Z,add,common.vaccinations.coverage_kinds.autumn_2022,cMBWaVHxQUmXKxLgJyJ8LF,__ -2022-11-21T09:11:50.358Z,add,common.vaccinations.coverage_kinds.fully_basisserie,cMBWaVHxQUmXKxLgJyJ8aT,__ -2022-11-21T09:11:50.359Z,delete,common.vaccinations.coverage_kinds.autumn_2022_vaccinated_percentage,NGAoa2ArqEakZsIxnSDBhz,__ -2022-11-21T09:11:50.360Z,delete,common.vaccinations.coverage_kinds.fully_vaccinated_percentage,jDbv30Y7XYH5kl8ZaoSZwv,__ -2022-11-23T09:37:10.394Z,add,common.vaccinations.coverage_kinds.primary_series,cMBWaVHxQUmXKxLgKD55A5,__ -2022-11-23T09:37:10.395Z,delete,common.vaccinations.coverage_kinds.fully_basisserie,cMBWaVHxQUmXKxLgJyJ8aT,__ diff --git a/packages/cms/src/schemas/measures/measures.ts b/packages/cms/src/schemas/measures/measures.ts index aa6f7fe5f8..5f61c45788 100644 --- a/packages/cms/src/schemas/measures/measures.ts +++ b/packages/cms/src/schemas/measures/measures.ts @@ -24,6 +24,12 @@ export const measures = { name: 'description', type: 'localeRichContentBlock', }, + { + title: 'Titel van de groepen', + name: 'collectionTitle', + type: 'localeString', + validation: REQUIRED, + }, { title: 'Groepen', description: 'De maatregelen zijn onderverdeeld in groepen', diff --git a/packages/cms/src/schemas/topical/trend-icon.ts b/packages/cms/src/schemas/topical/trend-icon.ts index 645ef52946..170e75659f 100644 --- a/packages/cms/src/schemas/topical/trend-icon.ts +++ b/packages/cms/src/schemas/topical/trend-icon.ts @@ -31,5 +31,20 @@ export const trendIcon = { }, validation: REQUIRED, }, + { + title: 'Intensiteit', + name: 'intensity', + description: 'Beschrijft de intensiteit van relatieve verandering ten opzichte van de vorige meeting.', + type: 'number', + options: { + list: [ + { value: 1, title: '1 pijltje gekleurd' }, + { value: 2, title: '2 pijltjes gekleurd' }, + { value: 3, title: '3 pijltjes gekleurd' }, + ], + layout: 'dropdown', + }, + validation: REQUIRED, + }, ], }; diff --git a/packages/icons/icons.md b/packages/icons/icons.md index ede4332b51..2d10992f0c 100644 --- a/packages/icons/icons.md +++ b/packages/icons/icons.md @@ -8,6 +8,7 @@ See below an overview of all the available icons in this package. This file is g | AlcoholVerkoop | | | Archive | | | Arrow | | +| ArrowWithIntensity | | | Arts | | | Avondklok | | | BarChart | | diff --git a/packages/icons/src/icon-name2filename.ts b/packages/icons/src/icon-name2filename.ts index 37d0b3dc65..b772ce9ff4 100644 --- a/packages/icons/src/icon-name2filename.ts +++ b/packages/icons/src/icon-name2filename.ts @@ -3,6 +3,7 @@ export type IconName = | 'AlcoholVerkoop' | 'Archive' | 'Arrow' + | 'ArrowWithIntensity' | 'Arts' | 'Avondklok' | 'BarChart' @@ -135,6 +136,7 @@ export const iconName2filename: Record = { AlcoholVerkoop: 'alcohol_verkoop.svg', Archive: 'archive.svg', Arrow: 'arrow.svg', + ArrowWithIntensity: 'arrow_with_intensity.svg', Arts: 'arts.svg', Avondklok: 'avondklok.svg', BarChart: 'bar_chart.svg', diff --git a/packages/icons/src/svg/arrow_with_intensity.svg b/packages/icons/src/svg/arrow_with_intensity.svg new file mode 100644 index 0000000000..956a73bc49 --- /dev/null +++ b/packages/icons/src/svg/arrow_with_intensity.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + \ No newline at end of file