Skip to content

Commit

Permalink
Merge pull request #39
Browse files Browse the repository at this point in the history
Add in-situ data
  • Loading branch information
clementprdhomme authored Nov 27, 2024
2 parents bbf8f88 + 512cab2 commit 01d4c1a
Show file tree
Hide file tree
Showing 37 changed files with 1,244 additions and 347 deletions.
1 change: 1 addition & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
"cmdk": "1.0.0",
"date-fns": "4.1.0",
"express": "4.21.1",
"jotai": "2.10.3",
"lodash-es": "4.17.21",
"mapbox-gl": "3.7.0",
"next": "14.2.15",
Expand Down
7 changes: 2 additions & 5 deletions client/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Jost, DM_Serif_Text } from "next/font/google";
import { NuqsAdapter } from "nuqs/adapters/next/app";

import ReactQueryProvider from "@/app/react-query-provider";
import Providers from "@/app/providers";
import Head from "@/components/head";

import type { Metadata } from "next";
Expand Down Expand Up @@ -38,9 +37,7 @@ export default function RootLayout({
<html lang="en" className={`${jost.variable} ${dmSerifText.variable}`}>
<Head />
<body>
<ReactQueryProvider>
<NuqsAdapter>{children}</NuqsAdapter>
</ReactQueryProvider>
<Providers>{children}</Providers>
</body>
</html>
);
Expand Down
19 changes: 19 additions & 0 deletions client/src/app/providers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"use client";

import { Provider as JotaiProvider } from "jotai";
import { NuqsAdapter } from "nuqs/adapters/next/app";
import { PropsWithChildren } from "react";

import ReactQueryProvider from "@/app/react-query-provider";

const Providers = ({ children }: PropsWithChildren) => {
return (
<ReactQueryProvider>
<NuqsAdapter>
<JotaiProvider>{children}</JotaiProvider>
</NuqsAdapter>
</ReactQueryProvider>
);
};

export default Providers;
30 changes: 30 additions & 0 deletions client/src/components/dataset-card/chart-sentence.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { GeoJsonProperties } from "geojson";
import { useMemo } from "react";

interface ChartSentenceprops {
sentence: string;
feature?: GeoJsonProperties;
}

const ChartSentence = ({ sentence, feature }: ChartSentenceprops) => {
const resolvedSentence = useMemo(() => {
let res = `${sentence}`; // Creating a copy

if (!!feature) {
Object.entries(feature).forEach(([key, value]) => {
res = res.replace(`{${key}}`, value);
});
}

return res;
}, [sentence, feature]);

return (
<div className="flex items-start justify-start gap-4 pl-8 text-xs">
{!!feature && <div className="shrink-0 font-medium">Selected point</div>}
<div>{resolvedSentence}</div>
</div>
);
};

export default ChartSentence;
132 changes: 132 additions & 0 deletions client/src/components/dataset-card/date-controls.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { getMonth } from "date-fns";
import { format } from "date-fns/format";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";

import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import MonthPicker from "@/components/ui/month-picker";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { cn } from "@/lib/utils";
import CalendarDaysIcon from "@/svgs/calendar-days.svg";
import ChevronDownIcon from "@/svgs/chevron-down.svg";
import PauseIcon from "@/svgs/pause.svg";
import PlayIcon from "@/svgs/play.svg";
import { DatasetLayersDataItem } from "@/types/generated/strapi.schemas";
import { LayerParamsConfig } from "@/types/layer";

interface DateControlsProps {
layer: DatasetLayersDataItem;
date: string;
onChangeDate: (date: string) => void;
}

const DateControls = ({ layer, date, onChangeDate }: DateControlsProps) => {
const [isAnimated, setIsAnimated] = useState(false);

const animationIntervalRef = useRef<NodeJS.Timeout | null>(null);
// Date that was selected before the animation is played
const dateBeforeAnimationRef = useRef<string | null>(null);

const dateRange = useMemo(() => {
const paramsConfig = layer.attributes!.params_config! as LayerParamsConfig;
return paramsConfig.find(({ key }) => key === "date-range")?.default as [string, string];
}, [layer]);

const onToggleAnimation = useCallback(() => {
const newIsAnimated = !isAnimated;

if (newIsAnimated) {
dateBeforeAnimationRef.current = date;
} else {
dateBeforeAnimationRef.current = null;
}

setIsAnimated(newIsAnimated);
}, [date, isAnimated, setIsAnimated]);

const onChangeSelectedDate = useCallback(
(date: string) => {
onChangeDate(date);
},
[onChangeDate],
);

// When the layer is animated, show each month of the year in a loop
useEffect(() => {
if (isAnimated) {
animationIntervalRef.current = setInterval(() => {
const newDate = format(new Date(date).setMonth((getMonth(date) + 1) % 12), "yyyy-MM-dd");

onChangeDate(newDate);
}, 500);
} else if (animationIntervalRef.current !== null) {
clearInterval(animationIntervalRef.current);
}

return () => {
if (animationIntervalRef.current !== null) {
clearInterval(animationIntervalRef.current);
}
};
}, [layer.id, date, isAnimated, onChangeDate]);

if (date === undefined && dateRange === undefined) {
return null;
}

return (
<div className="mt-1 flex items-center justify-between gap-4">
<Button
type="button"
variant="ghost"
size="auto"
className="hidden h-6 w-6 rounded-full border border-rhino-blue-950 hover:border-rhino-blue-800 hover:text-rhino-blue-800 lg:inline-flex"
aria-pressed={isAnimated}
onClick={onToggleAnimation}
>
<span className="sr-only">Play layer animation</span>
{!isAnimated && <PlayIcon className="!size-4 transition-colors" aria-hidden />}
{isAnimated && <PauseIcon className="!size-4 transition-colors" aria-hidden />}
</Button>
<div className="flex w-full items-center gap-2 lg:w-auto">
<Label
htmlFor={`layer-${layer.id}-date`}
className={cn({
"shrink-0 text-xs font-medium": true,
"pointer-events-none opacity-60": isAnimated,
})}
>
Displayed on map
</Label>
<Popover>
<PopoverTrigger asChild>
<Button
id={`layer-${layer.id}-date`}
type="button"
variant="yellow"
className="group/month-picker max-w-[220px] flex-grow justify-between px-3 disabled:bg-rhino-blue-50 disabled:text-rhino-blue-950/60 disabled:opacity-100 lg:flex-grow-0 xl:h-auto xl:py-1.5"
disabled={isAnimated}
>
<CalendarDaysIcon aria-hidden />
{format(isAnimated ? dateBeforeAnimationRef.current! : date, "MMMM, yyyy")}
<ChevronDownIcon
className="ml-auto group-data-[state=open]/month-picker:rotate-180"
aria-hidden
/>
</Button>
</PopoverTrigger>
<PopoverContent side="bottom" align="end" sideOffset={2} className="w-[220px]">
<MonthPicker
selected={isAnimated ? dateBeforeAnimationRef.current! : date}
minDate={dateRange[0]}
maxDate={dateRange[1]}
onSelect={onChangeSelectedDate}
/>
</PopoverContent>
</Popover>
</div>
</div>
);
};

export default DateControls;
48 changes: 48 additions & 0 deletions client/src/components/dataset-card/download-chart-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { useCallback } from "react";

import { Button } from "@/components/ui/button";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import GraphIcon from "@/svgs/graph.svg";

interface DownloadChartButtonProps {
data: unknown;
fileName: string;
disabled: boolean;
}

const DownloadChartButton = ({ data, fileName, disabled }: DownloadChartButtonProps) => {
const onClickSaveChartData = useCallback(() => {
const blob = new Blob([JSON.stringify(data)], { type: "application/json" });

const link = document.createElement("a");
link.download = fileName;
link.href = URL.createObjectURL(blob);
link.click();
link.remove();
}, [data, fileName]);

return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon-sm"
className="group/chart"
disabled={disabled}
onClick={onClickSaveChartData}
>
<span className="sr-only">Save chart data</span>
<GraphIcon
className="!size-4 transition-colors group-hover/chart:text-casper-blue-300"
aria-hidden
/>
</Button>
</TooltipTrigger>
<TooltipContent>Save chart data</TooltipContent>
</Tooltip>
</TooltipProvider>
);
};

export default DownloadChartButton;
33 changes: 33 additions & 0 deletions client/src/components/dataset-card/download-layer-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import Link from "next/link";

import { Button } from "@/components/ui/button";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import DownloadIcon from "@/svgs/download.svg";

interface DownloadLayerButtonProps {
link: string;
fileName: string;
}

const DownloadLayerButton = ({ link, fileName }: DownloadLayerButtonProps) => {
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon-sm" className="group/download" asChild>
<Link href={link} rel="noopener noreferrer" download={fileName}>
<span className="sr-only">Download</span>
<DownloadIcon
className="!size-4 transition-colors group-hover/download:text-casper-blue-300"
aria-hidden
/>
</Link>
</Button>
</TooltipTrigger>
<TooltipContent>Download dataset</TooltipContent>
</Tooltip>
</TooltipProvider>
);
};

export default DownloadLayerButton;
Loading

0 comments on commit 01d4c1a

Please sign in to comment.