Skip to content

Commit

Permalink
Merge pull request #266 from Vizzuality/SKY30-374-fe-hierarchical-dat…
Browse files Browse the repository at this point in the history
…a-table-to-simplify-layout-and-combine-duplicate-records

[SKY30-374] Tables sub-row support, preliminary set up for national/high seas tables, horizontal scroll indicators, fixes
  • Loading branch information
SARodrigues authored Jul 17, 2024
2 parents ace0fd7 + dec57d7 commit e6d2ff3
Show file tree
Hide file tree
Showing 10 changed files with 310 additions and 25 deletions.
41 changes: 41 additions & 0 deletions frontend/src/components/layer-preview/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { useQuery } from '@tanstack/react-query';
import axios from 'axios';

const MAPBOX_STYLES_ID = 'skytruth/clxk42ahk00as01qq1h4295jj';

const LayerPreview: React.FC<{
wdpaId: string;
bounds: [number, number, number, number];
}> = ({ wdpaId, bounds }) => {
const { data } = useQuery(['layer-preview', wdpaId], {
queryFn: async () => {
return axios
.get(`https://api.mapbox.com/styles/v1/${MAPBOX_STYLES_ID}/static/${bounds}/75x75`, {
responseType: 'blob',
params: {
setfilter: `['==', ['get', 'WDPAID'], ${wdpaId}]`,
layer_id: 'mpa-intermediate-simp', // ! update this
access_token: process.env.NEXT_PUBLIC_MAPBOX_API_TOKEN,
attribution: false,
logo: false,
// padding: 'auto',
},
})
.then((response) => response.data);
},
enabled: !!wdpaId,
});

const srcImage = data ? URL.createObjectURL(data) : null;

return (
<div
className="absolute top-0 left-0 h-full w-full bg-cover bg-center bg-no-repeat"
style={{
backgroundImage: `url(${srcImage})`,
}}
></div>
);
};

export default LayerPreview;
55 changes: 55 additions & 0 deletions frontend/src/components/positional-scroll/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { PropsWithChildren, useEffect, useState } from 'react';

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

export type ScrollPositions = 'start' | 'middle' | 'end';

export type PositionalScrollProps = PropsWithChildren<{
className?: string;
onXScrollPositionChange?: (position: ScrollPositions) => void;
onYScrollPositionChange?: (position: ScrollPositions) => void;
}>;

const PositionalScroll: React.FC<PositionalScrollProps> = ({
className,
onXScrollPositionChange,
onYScrollPositionChange,
children,
}) => {
const [xPosition, setXPosition] = useState<ScrollPositions>('start');
const [yPosition, setYPosition] = useState<ScrollPositions>('start');

useEffect(() => {
if (!onXScrollPositionChange) return;
onXScrollPositionChange(xPosition);
}, [onXScrollPositionChange, xPosition]);

useEffect(() => {
if (!onYScrollPositionChange) return;
onYScrollPositionChange(yPosition);
}, [onYScrollPositionChange, yPosition]);

const handleScroll = (event) => {
const target = event.target;

const xAtStartPosition = target.scrollLeft === 0;
const xAtEndPosition = target.scrollLeft === target.scrollWidth - target.clientWidth;

const yAtStartPosition = target.scrollTop === 0;
const yAtEndPosition = target.scrollTop === target.scrollHeight - target.clientHeight;

const calculatedXPosition = xAtStartPosition ? 'start' : xAtEndPosition ? 'end' : 'middle';
const calculatedYPosition = yAtStartPosition ? 'start' : yAtEndPosition ? 'end' : 'middle';

if (calculatedXPosition !== xPosition) setXPosition(calculatedXPosition);
if (calculatedYPosition !== yPosition) setYPosition(calculatedYPosition);
};

return (
<div className={cn(className)} onScroll={handleScroll}>
{children}
</div>
);
};

export default PositionalScroll;
8 changes: 6 additions & 2 deletions frontend/src/containers/map/content/details/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import CloseIcon from '@/styles/icons/close.svg';
import { FCWithMessages } from '@/types';
import { getGetLocationsQueryOptions, useGetLocations } from '@/types/generated/location';

import ScrollingIndicators from './table/scrolling-indicators';

const MapDetails: FCWithMessages = () => {
const t = useTranslations('containers.map');
const locale = useLocale();
Expand Down Expand Up @@ -113,8 +115,10 @@ const MapDetails: FCWithMessages = () => {
<Icon icon={CloseIcon} className="ml-2 h-3 w-3 pb-px " />
</Button>
</div>
<div className="mt-4">
<table.component />
<div className="relative z-0 mb-14">
<ScrollingIndicators className="mt-4 overflow-x-scroll">
<table.component />
</ScrollingIndicators>
</div>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { PropsWithChildren } from 'react';

import { Row } from '@tanstack/react-table';
import { GoTriangleDown } from 'react-icons/go';
import { LuCornerDownRight } from 'react-icons/lu';

import { GlobalRegionalTableColumns } from '@/containers/map/content/details/tables/global-regional/useColumns';
import { NationalHighseasTableColumns } from '@/containers/map/content/details/tables/national-highseas/useColumns';
import { cn } from '@/lib/classnames';

export type ExpansionControlsProps = PropsWithChildren<{
row: Row<GlobalRegionalTableColumns> | Row<NationalHighseasTableColumns>;
}>;

const ExpansionControls: React.FC<ExpansionControlsProps> = ({ row, children }) => {
const { depth, getIsExpanded, getCanExpand, getToggleExpandedHandler } = row;

const isParentRow = depth === 0;
const isRowExpandable = getCanExpand();
const isRowExpanded = getIsExpanded();
const toggleExpanded = getToggleExpandedHandler();

return (
<div className="flex items-center">
{isRowExpandable && (
<button
className="cursor pointer -ml-1.5 mr-1.5"
onClick={toggleExpanded}
aria-label={isRowExpanded ? 'Collapse sub-rows' : 'Expand sub-rows'}
>
<GoTriangleDown
className={cn({
'h-6 w-6 transition-transform': true,
'-rotate-90': !isRowExpanded,
})}
/>
</button>
)}
<span
className={cn({
'flex items-center pl-3': !isParentRow,
'ml-6': isParentRow && !isRowExpandable,
})}
>
{depth > 0 && <LuCornerDownRight className="mr-3 h-5 w-5" />}
{children}
</span>
</div>
);
};

export default ExpansionControls;
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { PropsWithChildren } from 'react';

const HeaderItem: React.FC<PropsWithChildren> = ({ children }) => {
return <span className="flex items-center gap-0">{children}</span>;
import { cn } from '@/lib/classnames';

type HeaderItemProps = PropsWithChildren<{
className?: string;
}>;

const HeaderItem: React.FC<HeaderItemProps> = ({ className, children }) => {
return <span className={cn('flex items-center gap-0', className)}>{children}</span>;
};

export default HeaderItem;
45 changes: 32 additions & 13 deletions frontend/src/containers/map/content/details/table/index.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { useRef } from 'react';
import { useCallback, useRef } from 'react';

import {
flexRender,
getCoreRowModel,
getExpandedRowModel,
getSortedRowModel,
useReactTable,
} from '@tanstack/react-table';
Expand All @@ -14,7 +15,7 @@ import { FCWithMessages } from '@/types';
// ! todo: type columns,data properly
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const MapTable: FCWithMessages = ({ columns, data }) => {
const MapTable: FCWithMessages = ({ columns, data, columnSeparators = null }) => {
const t = useTranslations('containers.map');

const tableRef = useRef<HTMLTableElement>();
Expand All @@ -31,16 +32,25 @@ const MapTable: FCWithMessages = ({ columns, data }) => {
},
],
},
getSubRows: (row) => row.subRows,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getExpandedRowModel: getExpandedRowModel(),
});

const hasData = table.getRowModel().rows?.length > 0;

const firstColumn = columns[0];
const secondColumn = columns[1];
const lastColumn = columns[columns.length - 1];

const shouldAddColumnSeparator = useCallback(
(columnId) => {
const isFirstColumn = columnId === firstColumn.accessorKey;
return columnSeparators ? columnSeparators?.includes(columnId) : isFirstColumn;
},
[columnSeparators, firstColumn.accessorKey]
);

return (
<table
ref={tableRef}
Expand All @@ -50,19 +60,20 @@ const MapTable: FCWithMessages = ({ columns, data }) => {
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id} className="shadow-[0_2px_0_-1px_rgb(0,0,0)]">
{headerGroup.headers.map((header) => {
const { id, column, index } = header;
const { id, column } = header;
const isFirstColumn = id === firstColumn.accessorKey;
const isSecondColumn = index === 1;
const isLastColumn = id === lastColumn.accessorKey;
const isMapColumn = column.id === 'map';

return (
<th
key={id}
ref={isFirstColumn ? firstColumnRef : null}
className={cn({
'h-10 py-3 pl-6 pr-16': true,
'border-r border-dashed border-black': shouldAddColumnSeparator(id),
'h-10': true,
'pl-6 pr-16': !isMapColumn,
'pl-0 pr-5': isFirstColumn,
'border-l border-dashed border-black': isSecondColumn,
'pr-0': isLastColumn,
})}
>
Expand All @@ -75,29 +86,37 @@ const MapTable: FCWithMessages = ({ columns, data }) => {
</thead>
<tbody>
{hasData &&
table.getRowModel().rows.map((row) => {
table.getRowModel().rows.map((row, idx) => {
const { depth } = row;
const isParentRow = depth === 0;
const isFirstRow = idx === 0;
const isLastRow = idx + 1 === table.getRowModel().rows.length;

return (
<tr
key={row.id}
className={cn({
'border-b border-t border-black': true,
'border-t-0': row.index === 0,
'border-t border-black': !isFirstRow,
'border-dashed': !isParentRow,
'border-b': isLastRow,
})}
>
{row.getVisibleCells().map((cell) => {
const { column } = cell;
const isFirstColumn = column.id === firstColumn.accessorKey;
const isSecondColumn = column.id === secondColumn.accessorKey;
const isLastColumn = column.id === lastColumn.accessorKey;
const isMapColumn = column.id === 'map';

return (
<td
key={cell.id}
className={cn({
'h-16 py-3 pl-6 pr-16': true,
'h-16 pl-6': true,
'pl-6 pr-16 ': !isMapColumn,
'-mt-px -mb-px': isMapColumn,
'pl-0 pr-5': isFirstColumn,
'border-l border-dashed border-black': isSecondColumn,
'pr-0': isLastColumn,
'border-r border-dashed border-black': shouldAddColumnSeparator(column.id),
})}
>
{flexRender(column.columnDef.cell, cell.getContext())}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { PropsWithChildren, useState } from 'react';

import { LuChevronLeft, LuChevronRight } from 'react-icons/lu';

import PositionalScroll, { ScrollPositions } from '@/components/positional-scroll';
import { cn } from '@/lib/classnames';

const COMMON_CLASSNAMES = {
border: 'absolute z-20 h-full border-r border-black',
iconWrapper: 'absolute border border-black p-px',
icon: 'h-4 w-4',
};

export type ScrollingIndicatorsProps = PropsWithChildren<{
className?: string;
}>;

const ScrollingIndicators: React.FC<ScrollingIndicatorsProps> = ({ className, children }) => {
const [xScrollPosition, setXScrollPosition] = useState<ScrollPositions>('start');

return (
<PositionalScroll className={cn(className)} onXScrollPositionChange={setXScrollPosition}>
{xScrollPosition !== 'end' && (
<>
<span className={cn(COMMON_CLASSNAMES.border, 'right-0 top-0')}>
<span
className={cn(COMMON_CLASSNAMES.iconWrapper, '-right-px top-0 -translate-y-full')}
>
<LuChevronRight className={COMMON_CLASSNAMES.icon} />
</span>
</span>
<span className={cn(COMMON_CLASSNAMES.border, 'right-0 bottom-0')}>
<span
className={cn(COMMON_CLASSNAMES.iconWrapper, '-right-px bottom-px translate-y-full')}
>
<LuChevronRight className={COMMON_CLASSNAMES.icon} />
</span>
</span>
</>
)}
{xScrollPosition !== 'start' && (
<>
<span className={cn(COMMON_CLASSNAMES.border, 'left-0 top-0')}>
<span className={cn(COMMON_CLASSNAMES.iconWrapper, 'left-0 top-0 -translate-y-full')}>
<LuChevronLeft className={COMMON_CLASSNAMES.icon} />
</span>
</span>
<span className={cn(COMMON_CLASSNAMES.border, 'left-0 bottom-0')}>
<span
className={cn(COMMON_CLASSNAMES.iconWrapper, 'left-0 bottom-px translate-y-full')}
>
<LuChevronLeft className={COMMON_CLASSNAMES.icon} />
</span>
</span>
</>
)}
{children}
</PositionalScroll>
);
};

export default ScrollingIndicators;
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ const useColumns = () => {
{
accessorKey: 'location',
header: ({ column }) => (
<HeaderItem>
<HeaderItem className="ml-1">
<SortingButton column={column} />
{t('name')}
<TooltipButton column={column} tooltips={tooltips} />
Expand All @@ -50,7 +50,7 @@ const useColumns = () => {
return (
<HeaderItem>
<Link
className="underline"
className="font-semibold underline"
href={`${PAGES.progressTracker}/${locationCode}?${searchParams.toString()}`}
>
{location}
Expand Down
Loading

0 comments on commit e6d2ff3

Please sign in to comment.