Skip to content

Commit

Permalink
EUDR detail page
Browse files Browse the repository at this point in the history
  • Loading branch information
andresgnlez committed Mar 14, 2024
1 parent 99faa1a commit 8409284
Show file tree
Hide file tree
Showing 28 changed files with 1,177 additions and 11 deletions.
1 change: 1 addition & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"@radix-ui/react-popover": "1.0.7",
"@radix-ui/react-radio-group": "1.1.3",
"@radix-ui/react-select": "2.0.0",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "1.0.2",
"@reduxjs/toolkit": "1.8.2",
"@tailwindcss/forms": "0.4.0",
Expand Down
24 changes: 24 additions & 0 deletions client/src/components/ui/separator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import * as React from 'react';
import * as SeparatorPrimitive from '@radix-ui/react-separator';

import { cn } from '@/lib/utils';

const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(({ className, orientation = 'horizontal', decorative = true, ...props }, ref) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn(
'shrink-0 bg-border',
orientation === 'horizontal' ? 'h-[1px] w-full' : 'h-full w-[1px]',
className,
)}
{...props}
/>
));
Separator.displayName = SeparatorPrimitive.Root.displayName;

export { Separator };
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { UTCDate } from '@date-fns/utc';
import { format } from 'date-fns';
import {
LineChart,
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
} from 'recharts';
import { useParams } from 'next/navigation';
import { useMemo, useState } from 'react';

import { EUDR_COLOR_RAMP } from '@/utils/colors';
import { useEUDRSupplier } from '@/hooks/eudr';
import { useAppSelector } from '@/store/hooks';
import { eudrDetail } from '@/store/features/eudr-detail';
import { Badge } from '@/components/ui/badge';
import { cn } from '@/lib/utils';

const DeforestationAlertsChart = (): JSX.Element => {
const [selectedPlots, setSelectedPlots] = useState<string[]>([]);
const { supplierId }: { supplierId: string } = useParams();
const {
filters: { dates },
} = useAppSelector(eudrDetail);
const { data } = useEUDRSupplier(
supplierId,
{
startAlertDate: dates.from,
endAlertDate: dates.to,
},
{
select: (data) => data?.alerts?.values,
},
);

const parsedData = data
?.map((item) => {
return {
...item,
...Object.fromEntries(item.plots.map((plot) => [plot.plotName, plot.alertCount])),
alertDate: new UTCDate(item.alertDate).getTime(),
};
})
?.sort((a, b) => new UTCDate(a.alertDate).getTime() - new UTCDate(b.alertDate).getTime());

const plotConfig = useMemo(() => {
if (!parsedData?.[0]) return [];

return Array.from(
new Set(parsedData.map((item) => item.plots.map((plot) => plot.plotName)).flat()),
).map((key, index) => ({
name: key,
color: EUDR_COLOR_RAMP[index] || '#000',
}));
}, [parsedData]);

return (
<>
<div className="flex flex-wrap gap-2">
{plotConfig.map(({ name, color }) => (
<Badge
key={name}
variant="secondary"
className={cn(
'flex cursor-pointer items-center space-x-1 rounded border border-gray-200 bg-white p-1 text-gray-900',
{
'bg-secondary/80': selectedPlots.includes(name),
},
)}
onClick={() => {
setSelectedPlots((prev) => {
if (prev.includes(name)) {
return prev.filter((item) => item !== name);
}
return [...prev, name];
});
}}
>
<span
className="inline-block h-[12px] w-[5px] rounded-[18px]"
style={{
background: color,
}}
/>
<span>{name}</span>
</Badge>
))}
</div>
<ResponsiveContainer width="100%" height={285}>
<LineChart
data={parsedData}
margin={{
top: 20,
bottom: 15,
}}
>
<CartesianGrid strokeDasharray="3 3" vertical={false} />
<XAxis
type="number"
scale="time"
dataKey="alertDate"
domain={[
new UTCDate(parsedData?.[0].alertDate).getTime(),
new UTCDate(parsedData?.[parsedData?.length - 1].alertDate).getTime(),
]}
tickFormatter={(value: string | number, x) => {
if (x === 0) return format(new UTCDate(value), 'LLL yyyy');
return format(new UTCDate(value), 'LLL');
}}
tickLine={false}
padding={{ left: 20, right: 20 }}
axisLine={false}
className="text-xs"
tickMargin={15}
/>
<YAxis tickLine={false} axisLine={false} label="(nº)" className="text-xs" />
<Tooltip labelFormatter={(v) => format(new UTCDate(v), 'dd/MM/yyyy')} />
{plotConfig?.map(({ name, color }) => {
return (
<Line
key={name}
dataKey={name}
stroke={color}
strokeWidth={3}
strokeOpacity={selectedPlots.length ? (selectedPlots.includes(name) ? 1 : 0.2) : 1}
connectNulls
/>
);
})}
</LineChart>
</ResponsiveContainer>
</>
);
};

export default DeforestationAlertsChart;
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { useParams } from 'next/navigation';
import { format } from 'date-fns';
import { UTCDate } from '@date-fns/utc';
import { BellRing } from 'lucide-react';

import DeforestationAlertsChart from './chart';

import { useEUDRSupplier } from '@/hooks/eudr';
import { eudrDetail } from '@/store/features/eudr-detail';
import { useAppSelector } from '@/store/hooks';

const dateFormatter = (date: string) => format(new UTCDate(date), "do 'of' MMMM yyyy");

const DeforestationAlerts = (): JSX.Element => {
const { supplierId }: { supplierId: string } = useParams();
const {
filters: { dates },
} = useAppSelector(eudrDetail);
const { data } = useEUDRSupplier(
supplierId,
{
startAlertDate: dates.from,
endAlertDate: dates.to,
},
{
select: (data) => data?.alerts,
},
);

return (
<section className="space-y-4 rounded-xl border border-gray-100 p-7 shadow-md">
<h4 className="font-medium">Deforestation alerts detected within the smallholders</h4>
{data?.totalAlerts && (
<div className="rounded-xl bg-orange-100 px-6 py-4 text-xs">
There were <span className="font-bold">{data?.totalAlerts}</span> deforestation alerts
reported for the supplier between the{' '}
<span className="font-bold">{dateFormatter(data.startAlertDate)}</span> and the{' '}
<div className="flex items-center space-x-2">
<span className="font-bold">{dateFormatter(data.endAlertDate)}</span>.
<BellRing className="h-5 w-5 fill-black" />
</div>
</div>
)}
<DeforestationAlertsChart />
</section>
);
};

export default DeforestationAlerts;
11 changes: 11 additions & 0 deletions client/src/containers/analysis-eudr-detail/filters/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import YearsRange from './years-range';

const EUDRDetailFilters = () => {
return (
<div className="flex space-x-2">
<YearsRange />
</div>
);
};

export default EUDRDetailFilters;
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import React, { useCallback, useMemo } from 'react';
import { UTCDate } from '@date-fns/utc';
import { ChevronDown } from 'lucide-react';
import { format } from 'date-fns';

import { useAppDispatch, useAppSelector } from 'store/hooks';
import { eudrDetail, setFilters } from 'store/features/eudr-detail';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { Button } from '@/components/ui/button';
import { Calendar } from '@/components/ui/calendar';

import type { DateRange } from 'react-day-picker';
const dateFormatter = (date: Date) => format(date, 'yyyy-MM-dd');

// ! the date range is hardcoded for now
export const DATES_RANGE = ['2020-12-31', dateFormatter(new Date())];

const DatesRange = (): JSX.Element => {
const dispatch = useAppDispatch();
const {
filters: { dates },
} = useAppSelector(eudrDetail);

const handleDatesChange = useCallback(
(dates: DateRange) => {
if (dates) {
dispatch(
setFilters({
dates: {
from: dateFormatter(dates.from),
to: dateFormatter(dates.to),
},
}),
);
}
},
[dispatch],
);

const datesToDate = useMemo(() => {
return {
from: dates.from ? new UTCDate(dates.from) : undefined,
to: dates.to ? new UTCDate(dates.to) : undefined,
};
}, [dates]);

return (
<Popover>
<PopoverTrigger asChild>
<Button
variant="ghost"
className="h-auto space-x-1 border border-gray-200 bg-white shadow-sm"
>
<span className="text-gray-500">
Deforestation alerts from <span className="text-gray-900">{dates.from || '-'}</span> to{' '}
<span className="text-gray-900">{dates.to || '-'}</span>
</span>
<ChevronDown className="h-4 w-4" />
</Button>
</PopoverTrigger>
<PopoverContent className="flex w-auto space-x-2" align="start">
<Calendar
mode="range"
numberOfMonths={2}
disabled={{
before: new UTCDate(DATES_RANGE[0]),
after: new UTCDate(DATES_RANGE[1]),
}}
selected={datesToDate}
onSelect={handleDatesChange}
/>
</PopoverContent>
</Popover>
);
};

export default DatesRange;
Loading

0 comments on commit 8409284

Please sign in to comment.