Skip to content

Commit

Permalink
Merge pull request #321 from Vizzuality/SKY30-479-fe-implementation-o…
Browse files Browse the repository at this point in the history
…f-data-disclaimer-text

Add a terrestrial data disclaimer dialog to the Progress tracker
  • Loading branch information
clementprdhomme authored Oct 11, 2024
2 parents 19dcd5e + ee2a716 commit 6fd91d5
Show file tree
Hide file tree
Showing 11 changed files with 145 additions and 29 deletions.
52 changes: 52 additions & 0 deletions frontend/src/components/terrestrial-data-disclaimer-dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { useAtom } from 'jotai';
import { useTranslations } from 'next-intl';

import { Button } from '@/components/ui/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import Icon from '@/components/ui/icon';
import { terrestrialDataDisclaimerDialogAtom } from '@/containers/map/store';
import Notification from '@/styles/icons/notification.svg';

interface TerrestrialDataDisclaimerDialogProps {}

const TerrestrialDataDisclaimerDialog = ({}: TerrestrialDataDisclaimerDialogProps) => {
const t = useTranslations('pages.progress-tracker');
const [, setOpen] = useAtom(terrestrialDataDisclaimerDialogAtom);

return (
<Dialog open>
<DialogContent closable={false}>
<DialogHeader className="flex-row gap-2 font-bold text-red">
<Icon icon={Notification} className="h-6 w-6" />
{t('important-notification')}
</DialogHeader>
<DialogTitle className="max-w-[350px]">
{t('terrestrial-data-disclaimer-dialog-title')}
</DialogTitle>
<DialogDescription>
{t.rich('terrestrial-data-disclaimer-dialog-content', {
br: () => <br />,
})}
</DialogDescription>
<DialogFooter>
<Button
type="button"
className="font-mono text-xs font-normal normal-case"
onClick={() => setOpen(false)}
>
{t('i-understand')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};

export default TerrestrialDataDisclaimerDialog;
13 changes: 10 additions & 3 deletions frontend/src/components/tooltip-button/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Fragment, useState } from 'react';
import { Fragment, ReactNode, useState } from 'react';

import Linkify from 'react-linkify';

Expand All @@ -20,9 +20,15 @@ interface TooltipButtonProps {
className?: string;
text: string;
sources?: Source | Source[];
extraContent?: ReactNode;
}

const TooltipButton: FCWithMessages<TooltipButtonProps> = ({ className, text, sources }) => {
const TooltipButton: FCWithMessages<TooltipButtonProps> = ({
className,
text,
sources,
extraContent,
}) => {
const t = useTranslations('components.tooltip-button');

const [isTooltipOpen, setIsTooltipOpen] = useState<boolean>(false);
Expand Down Expand Up @@ -65,7 +71,7 @@ const TooltipButton: FCWithMessages<TooltipButtonProps> = ({ className, text, so

{Array.isArray(sources) && (
<div className="">
<span>Data sources: </span>
<span>{t('data-sources:')} </span>
{sources.map(({ id, title, url }, index) => (
<Fragment key={id}>
<a
Expand All @@ -91,6 +97,7 @@ const TooltipButton: FCWithMessages<TooltipButtonProps> = ({ className, text, so
{t('data-source')}
</a>
)}
{extraContent}
</PopoverContent>
</Popover>
);
Expand Down
34 changes: 15 additions & 19 deletions frontend/src/components/ui/dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const DialogOverlay = React.forwardRef<
<DialogPrimitive.Overlay
ref={ref}
className={cn(
'fixed inset-0 z-50 bg-white/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
'fixed inset-0 z-50 bg-black/30 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
className
)}
{...props}
Expand All @@ -31,23 +31,27 @@ DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;

const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & {
closable?: boolean;
}
>(({ className, children, closable = true, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
'fixed left-[50%] top-[50%] z-50 grid w-auto max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border border-slate-200 bg-white p-6 pt-10 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
'fixed left-[50%] top-[50%] z-50 grid w-auto max-w-[600px] translate-x-[-50%] translate-y-[-50%] gap-6 border border-black bg-white p-10 duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%]',
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="focus:ring-slate-950 absolute right-4 top-4 rounded-sm opacity-50 ring-offset-white transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-slate-100 data-[state=open]:text-slate-500">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
{closable && (
<DialogPrimitive.Close className="focus:ring-slate-950 absolute right-4 top-4 rounded-sm opacity-50 ring-offset-white transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-slate-100 data-[state=open]:text-slate-500">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
));
Expand All @@ -60,7 +64,7 @@ DialogHeader.displayName = 'DialogHeader';

const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', className)}
className={cn('mt-4 flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', className)}
{...props}
/>
);
Expand All @@ -70,23 +74,15 @@ const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn('text-lg font-semibold leading-none tracking-tight', className)}
{...props}
/>
<DialogPrimitive.Title ref={ref} className={cn('text-xl font-black', className)} {...props} />
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;

const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn('text-sm text-slate-500', className)}
{...props}
/>
<DialogPrimitive.Description ref={ref} className={className} {...props} />
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;

Expand Down
8 changes: 6 additions & 2 deletions frontend/src/components/widget/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ComponentProps, PropsWithChildren, useMemo } from 'react';
import { ComponentProps, PropsWithChildren, ReactNode, useMemo } from 'react';

import { timeFormatLocale } from 'd3-time-format';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
Expand Down Expand Up @@ -31,6 +31,7 @@ type WidgetProps = {
errorMessage?: ComponentProps<typeof NoData>['message'];
info?: ComponentProps<typeof TooltipButton>['text'];
sources?: ComponentProps<typeof TooltipButton>['sources'];
tooltipExtraContent?: ReactNode;
};

const d3Locales = {
Expand All @@ -50,6 +51,7 @@ const Widget: FCWithMessages<PropsWithChildren<WidgetProps>> = ({
errorMessage = undefined,
info,
sources,
tooltipExtraContent,
children,
}) => {
const t = useTranslations('components.widget');
Expand All @@ -69,7 +71,9 @@ const Widget: FCWithMessages<PropsWithChildren<WidgetProps>> = ({
<div className="pt-2">
<span className="flex items-baseline justify-between">
{title && <h2 className="font-sans text-xl font-bold leading-tight">{title}</h2>}
{(info || sources) && <TooltipButton text={info} sources={sources} />}
{(info || sources) && (
<TooltipButton text={info} sources={sources} extraContent={tooltipExtraContent} />
)}
</span>
{!showNoData && lastUpdated && (
<span className="text-xs">{t('updated-on', { date: formattedLastUpdated })}</span>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import { useMemo } from 'react';

import { useAtom } from 'jotai';
import { groupBy } from 'lodash-es';
import { useLocale, useTranslations } from 'next-intl';

import ConservationChart from '@/components/charts/conservation-chart';
import { Button } from '@/components/ui/button';
import Icon from '@/components/ui/icon';
import Widget from '@/components/widget';
import { terrestrialDataDisclaimerDialogAtom } from '@/containers/map/store';
import { useSyncMapContentSettings } from '@/containers/map/sync-settings';
import { formatKM } from '@/lib/utils/formats';
import { formatPercentage } from '@/lib/utils/formats';
import Notification from '@/styles/icons/notification.svg';
import { FCWithMessages } from '@/types';
import { useGetDataInfos } from '@/types/generated/data-info';
import { useGetProtectionCoverageStats } from '@/types/generated/protection-coverage-stat';
Expand All @@ -29,6 +33,8 @@ const TerrestrialConservationWidget: FCWithMessages<TerrestrialConservationWidge

const [{ tab }, setSettings] = useSyncMapContentSettings();

const [, setDisclaimerDialogOpen] = useAtom(terrestrialDataDisclaimerDialogAtom);

const { data, isFetching } = useGetProtectionCoverageStats<
ProtectionCoverageStatListResponseDataItem[]
>(
Expand Down Expand Up @@ -163,6 +169,19 @@ const TerrestrialConservationWidget: FCWithMessages<TerrestrialConservationWidge
loading={isFetching}
info={metadata?.info}
sources={metadata?.sources}
tooltipExtraContent={
<Button
type="button"
variant="text-link"
size="sm"
className="-mt-3 justify-start gap-1.5 px-0 py-0 text-xs font-bold normal-case text-red"
onClick={() => setDisclaimerDialogOpen(true)}
>
<Icon icon={Notification} className="h-4 w-4" />

{t('data-disclaimer')}
</Button>
}
>
{stats && (
<div className="mt-6 mb-4 flex flex-col">
Expand Down
10 changes: 9 additions & 1 deletion frontend/src/containers/map/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { MapLayerMouseEvent } from 'react-map-gl';

import { Feature } from 'geojson';
import { atom } from 'jotai';
import { atomWithReset } from 'jotai/utils';
import { atomWithReset, atomWithStorage } from 'jotai/utils';

import { CustomMapProps } from '@/components/map/types';
import { LayerResponseDataObject } from '@/types/generated/strapi.schemas';
Expand Down Expand Up @@ -40,3 +40,11 @@ export const modellingAtom = atomWithReset<{
data: null,
errorMessage: undefined,
});

/**
* Whether the disclaimer dialog should be visible
*/
export const terrestrialDataDisclaimerDialogAtom = atomWithStorage(
'terrestrial-data-disclaimer-dialog',
true
);
20 changes: 19 additions & 1 deletion frontend/src/layouts/map.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,29 @@
import { PropsWithChildren, useEffect } from 'react';

import dynamic from 'next/dynamic';

import { useAtomValue } from 'jotai';
import { useResetAtom } from 'jotai/utils';
import { useTranslations } from 'next-intl';

import Head from '@/components/head';
import Header from '@/components/header';
import Content from '@/containers/map/content';
import Sidebar from '@/containers/map/sidebar';
import { drawStateAtom, modellingAtom } from '@/containers/map/store';
import {
drawStateAtom,
modellingAtom,
terrestrialDataDisclaimerDialogAtom,
} from '@/containers/map/store';
import { FCWithMessages } from '@/types';

const TerrestrialDataDisclaimerDialog = dynamic(
() => import('@/components/terrestrial-data-disclaimer-dialog'),
{
ssr: false,
}
);

const LAYOUT_TYPES = {
progress_tracker: 'progress-tracker',
conservation_builder: 'conservation-builder',
Expand All @@ -30,6 +44,7 @@ const MapLayout: FCWithMessages<PropsWithChildren<MapLayoutProps>> = ({

const resetModelling = useResetAtom(modellingAtom);
const resetDrawState = useResetAtom(drawStateAtom);
const terrestrialDataDisclaimerDialogOpen = useAtomValue(terrestrialDataDisclaimerDialogAtom);

useEffect(() => {
if (type !== LAYOUT_TYPES.conservation_builder) {
Expand All @@ -48,6 +63,9 @@ const MapLayout: FCWithMessages<PropsWithChildren<MapLayoutProps>> = ({
}
description={description}
/>
{type === LAYOUT_TYPES.progress_tracker && terrestrialDataDisclaimerDialogOpen && (
<TerrestrialDataDisclaimerDialog />
)}
<div className="flex h-screen w-screen flex-col">
<div className="flex-shrink-0">
<Header />
Expand Down
1 change: 1 addition & 0 deletions frontend/src/pages/progress-tracker/[locationCode].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ ProgressTrackerPage.layout = {

return {
title: location?.[locationNameField],
type: 'progress-tracker',
};
},
};
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/styles/icons/notification.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions frontend/tailwind.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ module.exports = {
orange: '#FD8E28',
violet: '#AD6CFF',
'gray-300': '#999999',
red: '#F43F4C',
},
maxWidth: {
screen: '100vw',
Expand Down
13 changes: 10 additions & 3 deletions frontend/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,12 @@
"outro-button": "Get in touch",
"looking-for": "I am looking for..."
},
"progress-tracker": {},
"progress-tracker": {
"terrestrial-data-disclaimer-dialog-title": "Terrestrial Conservation Coverage Data Disclaimer",
"terrestrial-data-disclaimer-dialog-content": "Protected area totals and trends in this dataset are derived from publicly available boundaries sourced from Protected Planet. However, some countries restrict the release of detailed protected area boundaries, leading to differences between our totals and those reported by the World Database on Protected Areas.<br></br><br></br>Due to these data restrictions, our protected area totals are lower both globally and in regions where boundaries are not publicly accessible.",
"i-understand": "I understand",
"important-notification": "Important notification"
},
"conservation-builder": {}
},
"containers": {
Expand Down Expand Up @@ -230,7 +235,8 @@
"explore-terrestrial-conservation": "Explore Terrestrial Conservation",
"explore-marine-conservation": "Explore Marine Conservation",
"terrestrial-existing-conservation": "Existing terrestrial conservation coverage",
"not-assessed": "Not assessed"
"not-assessed": "Not assessed",
"data-disclaimer": "Data disclaimer"
},
"map-sidebar-layers-panel": {
"layers": "Layers",
Expand Down Expand Up @@ -360,7 +366,8 @@
},
"tooltip-button": {
"info": "Info",
"data-source": "Data source"
"data-source": "Data source",
"data-sources:": "Data sources:"
},
"ui-carousel": {
"previous-slide": "Previous slide",
Expand Down

0 comments on commit 6fd91d5

Please sign in to comment.