Skip to content

Commit

Permalink
feat: create BlockNumber filter (#522)
Browse files Browse the repository at this point in the history
* refactor(web): create Option type for Dropdown options

* chore(web): update changeset

* refactor(web): use label in Dropdown selected option display

* refactor(web): update Dropdown component main filename

* refactor(web): remove nested /components folder in Dropdown

* feat(web): render value when no label in Dropdown

* feat(web): add prefix slot to Dropdown option

* feat(web): add filter panel with rollup filter in PaginatedTable

* chore(web): update changeset

* refactor(web): rename main PaginedTable filename

* refactor(web): rename main FilersPanel filename

* refactor(web): update RollupFilter component to fetch rollups from API

* feat(web): add filters to query params

* fix(web): pages number calculation in PaginatedTable

* fix(web): pages number calculation in PaginatedTable

* feat(web): install React DatePicker Tailwind library

* feat(web): add full width prop to Button

* feat(web): add custom height prop to Dropdown

* feat(web): add Timestamp filter to FilterPanel

* chore(web): update changeset

* feat(web): add timestamp filters to URL

* fix(web): Skeleton custom height not working

* refactor(web): simplify dropdowns options

* refactor(web): reduce contitions to filter in Filters

* fix(web): reset filters when all are reset after being filtered

* feat(web): create BlockNumber filter

* feat(web): move block number validations to filters form

* chore(web): update changeset

* fix(web): reset filters when all are reset after being filtered

* feat(web): add clear button for filter

* fix(web): clear/filter buttons responsiveness

* fix(web): clear/filter buttons responsiveness for desktop

* fix(web): fix block number filter responsiveness

* refactor(web): decouple Filters from PaginatedTable

* fix(web): fix filters reponsiveness

* fix(web): align timestamp filter input style with the dropdown component for consistency

* fix(web): convert filter dates to ISO format before adding them as query parameters

* fix(web): rollback global styles

* refactor(web): refactor input components

- Separate number input into its own component.
- Perform number input validation logic within the component directly.
- Set up input style variants.

* fix(web): fix hint light color

* fix(web): align inputs styles

* chore(web): remove unused filters utilities

* fix(web): separate `Header` and `Filters` from `PaginatedTable`

* feat(web): enhance block number filter with responsive design

* feat(web): enhance filters bar with responsive design

* refactor(web): resolve review comments

* refactor(web): encapsulate `Filters` state management into a reducer

* fix(web): align input clear icons colors

---------

Co-authored-by: Luis Herasme <[email protected]>
Co-authored-by: PJColombo <[email protected]>
  • Loading branch information
3 people authored Sep 3, 2024
1 parent d0b07e1 commit 9c56d5b
Show file tree
Hide file tree
Showing 20 changed files with 508 additions and 245 deletions.
5 changes: 5 additions & 0 deletions .changeset/shy-clouds-roll.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@blobscan/web": minor
---

Added BlockNumber filter to PaginatedTable filter panel
12 changes: 9 additions & 3 deletions apps/web/src/components/Dropdown/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,21 @@ export const Dropdown: React.FC<DropdownProps> = function ({
<ListboxButton
className={`relative h-9 ${
width ?? DEFAULT_WIDTH
} flex cursor-pointer items-center justify-between rounded-lg border border-transparent bg-controlBackground-light pl-2 pr-12 text-left text-sm shadow-md hover:border hover:border-controlBackground-light focus:outline-none focus-visible:border-indigo-500 focus-visible:ring-2 focus-visible:ring-white active:border-controlBorderHighlight-dark ui-open:border-controlActive-light dark:bg-controlBackground-dark dark:hover:border-controlBorderHighlight-dark dark:ui-open:border-controlActive-dark`}
} flex cursor-pointer items-center justify-between rounded-lg border border-transparent bg-controlBackground-light pl-2 pr-12 text-left text-sm shadow-md hover:border hover:border-controlBorderHighlight-light focus:outline-none focus-visible:border-indigo-500 focus-visible:ring-2 focus-visible:ring-white active:border-controlBorderHighlight-dark ui-open:border-controlActive-light dark:bg-controlBackground-dark dark:hover:border-controlBorderHighlight-dark dark:ui-open:border-controlActive-dark`}
>
<div className="truncate align-middle">
{selected ? selected.label ?? selected.value : placeholder}
{selected ? (
selected.label ?? selected.value
) : (
<div className="text-hint-light dark:text-hint-dark">
{placeholder}
</div>
)}
</div>
<div className="absolute inset-y-0 right-0 flex items-center pr-2">
{clearable && selected ? (
<XMarkIcon
className="h-5 w-5 text-contentTertiary-light opacity-60 hover:text-iconHighlight-light hover:opacity-100 dark:text-contentTertiary-dark dark:hover:text-iconHighlight-dark"
className="h-5 w-5 text-icon-light hover:text-iconHighlight-light dark:text-icon-dark dark:hover:text-iconHighlight-dark"
onClick={(e) => {
e.stopPropagation();
onChange(null);
Expand Down
18 changes: 18 additions & 0 deletions apps/web/src/components/Filters/BlockNumberFilter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { FC } from "react";

import type { NumberRangeInputProps } from "../Inputs/NumberRangeInput";
import { NumberRangeInput } from "../Inputs/NumberRangeInput";

type BlockNumberFilterProps = Pick<NumberRangeInputProps, "range" | "onChange">;

export const BlockNumberFilter: FC<BlockNumberFilterProps> = function (props) {
return (
<NumberRangeInput
className="w-full"
type="uint"
inputStartProps={{ placeholder: "Start Block" }}
inputEndProps={{ placeholder: "End Block" }}
{...props}
/>
);
};
2 changes: 1 addition & 1 deletion apps/web/src/components/Filters/RollupFilter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export const RollupFilter: FC<RollupFilterProps> = function ({
options={ROLLUP_OPTIONS}
onChange={onChange}
placeholder="Rollup"
width="w-40"
width="w-full"
clearable
/>
);
Expand Down
59 changes: 28 additions & 31 deletions apps/web/src/components/Filters/TimestampFilter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,36 +12,33 @@ export const TimestampFilter: FC<TimestampFilterProps> = function ({
const isValueSet = value?.startDate || value?.endDate;

return (
<div className="min-w-64">
<Datepicker
primaryColor="purple"
value={value}
onChange={onChange}
inputClassName={cn(
"h-9 w-full pl-2 pr-12",
"text-left text-sm",
"cursor-pointer",
"rounded-lg border border-transparent shadow-md",
"hover:border hover:border-controlBackground-light dark:hover:border-controlBorderHighlight-dark",
"focus:outline-none focus-visible:border-indigo-500 focus-visible:ring-2 focus-visible:ring-white",
"ui-open:border-controlActive-light dark:ui-open:border-controlActive-dark",
"bg-controlBackground-light dark:bg-controlBackground-dark",
"active:border-controlBorderHighlight-dark"
)}
toggleClassName={(defaultToggleClassName) =>
cn(
defaultToggleClassName,
"opacity-60",
"text-contentTertiary-light dark:text-contentTertiary-dark",
{
"hover:text-iconHighlight-light hover:opacity-100 dark:hover:text-iconHighlight-dark":
isValueSet,
}
)
}
containerClassName="relative tailwind-datepicker"
placeholder="Start date - End date"
/>
</div>
<Datepicker
primaryColor="purple"
value={value}
onChange={onChange}
inputClassName={cn(
"h-9 w-full pl-2 pr-12",
"text-left text-sm placeholder:text-sm",
"cursor-pointer",
"rounded-lg border border-transparent shadow-md",
"dark:placeholder:text-hint-dark placeholder:text-hint-light hover:border hover:border-controlBackground-light dark:hover:border-controlBorderHighlight-dark",
"focus:outline-none focus-visible:border-indigo-500 focus:border-controlBorderHighlight-dark",
"ui-open:border-controlActive-light dark:ui-open:border-controlActive-dark",
"bg-controlBackground-light dark:bg-controlBackground-dark",
"active:border-controlBorderHighlight-dark",
"focus:dark:border-controlBorderHighlight-dark focus:ring-transparent"
)}
toggleClassName={(defaultToggleClassName) =>
cn(defaultToggleClassName, "text-icon-light dark:text-icon-dark", {
"hover:text-iconHighlight-light hover:opacity-100 dark:hover:text-iconHighlight-dark":
isValueSet,
})
}
containerClassName={cn(
"relative",
"[&>div>div]:dark:bg-controlBackground-dark [&>div>div]:border-controlBorder-light [&>div>div]:dark:border-transparent"
)}
placeholder="Start date - End date"
/>
);
};
184 changes: 131 additions & 53 deletions apps/web/src/components/Filters/index.tsx
Original file line number Diff line number Diff line change
@@ -1,43 +1,78 @@
import { useEffect, useState } from "react";
import { useEffect, useReducer } from "react";
import type { FC } from "react";
import { useRouter } from "next/router";
import type { DateValueType } from "react-tailwindcss-datepicker";
import type { DateRangeType } from "react-tailwindcss-datepicker";
import type { UrlObject } from "url";

import { Button } from "~/components/Button";
import { useQueryParams } from "~/hooks/useQueryParams";
import { getISODate } from "~/utils";
import { Card } from "../Cards/Card";
import type { Option } from "../Dropdown";
import type { NumberRange } from "../Inputs/NumberRangeInput";
import { BlockNumberFilter } from "./BlockNumberFilter";
import { ROLLUP_OPTIONS, RollupFilter } from "./RollupFilter";
import { TimestampFilter } from "./TimestampFilter";

type FiltersState = {
rollup: Option | null;
timestampRange: DateValueType | null;
timestampRange: DateRangeType | null;
blockNumberRange: NumberRange | null;
};

type ClearAction<V extends keyof FiltersState> = {
type: "CLEAR";
payload?: { field: V };
};

type UpdateAction = {
type: "UPDATE";
payload: Partial<FiltersState>;
};

type FiltersAction<V extends keyof FiltersState> =
| ClearAction<V>
| UpdateAction;

const INIT_STATE: FiltersState = {
rollup: null,
timestampRange: {
startDate: null,
endDate: null,
startDate: null,
},
blockNumberRange: null,
};

function reducer<V extends keyof FiltersState>(
prevState: FiltersState,
{ type, payload }: FiltersAction<V>
): FiltersState {
switch (type) {
case "CLEAR":
return payload
? {
...prevState,
[payload.field]: null,
}
: { ...INIT_STATE };
case "UPDATE":
return {
...prevState,
...payload,
};
}
}

export const Filters: FC = function () {
const router = useRouter();
const queryParams = useQueryParams();

const [filters, setFilters] = useState<FiltersState>(INIT_STATE);
const [filters, dispatch] = useReducer(reducer, INIT_STATE);
const disableClear =
!filters.rollup &&
!filters.timestampRange?.startDate &&
!filters.timestampRange?.endDate;
!filters.rollup && !filters.timestampRange && !filters.blockNumberRange;

const handleFilter = () => {
const query: UrlObject["query"] = {};
const { rollup, timestampRange } = filters;
const { endDate, startDate } = timestampRange || {};
const { rollup, timestampRange, blockNumberRange } = filters;

if (rollup) {
if (rollup.value === "null") {
Expand All @@ -47,12 +82,28 @@ export const Filters: FC = function () {
}
}

if (startDate) {
query.startDate = getISODate(startDate);
if (timestampRange) {
const { startDate, endDate } = timestampRange;

if (startDate) {
query.startDate = getISODate(startDate);
}

if (endDate) {
query.endDate = getISODate(endDate);
}
}

if (endDate) {
query.endDate = getISODate(endDate);
if (blockNumberRange) {
const { start, end } = blockNumberRange;

if (start) {
query.startBlock = start;
}

if (end) {
query.endBlock = end;
}
}

router.push({
Expand All @@ -61,60 +112,87 @@ export const Filters: FC = function () {
});
};

const handleClear = () => {
setFilters(INIT_STATE);
};

const handleRollupFilterChange = (newRollup: Option) => {
setFilters((prevState) => ({ ...prevState, rollup: newRollup }));
};

const handleTimestampRangeFilterChange = (newRange: DateValueType) => {
setFilters((prevState) => ({ ...prevState, timestampRange: newRange }));
};

useEffect(() => {
const { rollup, from, startDate, endDate } = queryParams;
const { rollup, from, startDate, endDate, startBlock, endBlock } =
queryParams;
const newFilters: Partial<FiltersState> = {};

if (rollup || from) {
const rollupOption = ROLLUP_OPTIONS.find(
(opt) => opt.value === rollup || opt.value === from
);

if (rollupOption) {
setFilters((prevFilters) => ({ ...prevFilters, rollup: rollupOption }));
newFilters.rollup = rollupOption;
}
}

if (startDate || endDate) {
setFilters((prevFilters) => ({
...prevFilters,
timestampRange: {
startDate: startDate ? new Date(startDate) : null,
endDate: endDate ? new Date(endDate) : null,
},
}));
newFilters.timestampRange = {
startDate: startDate ? new Date(startDate) : null,
endDate: endDate ? new Date(endDate) : null,
};
}

if (startBlock || endBlock) {
newFilters.blockNumberRange = {
start: startBlock,
end: endBlock,
};
}

dispatch({ type: "UPDATE", payload: newFilters });
}, [queryParams]);

return (
<div className="flex w-full flex-col justify-between gap-2 rounded-lg bg-slate-50 p-2 dark:bg-primary-900 sm:flex-row">
<div className="flex w-full flex-col items-center gap-2 md:flex-row">
<RollupFilter
selected={filters.rollup}
onChange={handleRollupFilterChange}
/>
<TimestampFilter
value={filters.timestampRange}
onChange={handleTimestampRangeFilterChange}
/>
</div>
<div className="flex flex-row gap-2">
<Button variant="outline" onClick={handleClear} disabled={disableClear}>
Clear
</Button>
<Button onClick={handleFilter}>Filter</Button>
<Card>
<div className="flex flex-col justify-between gap-4 lg:flex-row lg:gap-0">
<div className="flex w-full flex-col items-center gap-2 md:flex-row">
<div className="w-full md:w-40">
<RollupFilter
selected={filters.rollup}
onChange={(newRollup) =>
dispatch({ type: "UPDATE", payload: { rollup: newRollup } })
}
/>
</div>
<div className="w-full md:w-64">
<TimestampFilter
value={filters.timestampRange}
onChange={(newTimestampRange) =>
dispatch({
type: "UPDATE",
payload: { timestampRange: newTimestampRange },
})
}
/>
</div>
<div className="w-full md:w-52">
<BlockNumberFilter
range={filters.blockNumberRange}
onChange={(newBlockNumberRange) =>
dispatch({
type: "UPDATE",
payload: { blockNumberRange: newBlockNumberRange },
})
}
/>
</div>
</div>
<div className="flex flex-col gap-2 md:flex-row">
<Button
className="w-full lg:w-auto"
variant="outline"
onClick={() => dispatch({ type: "CLEAR" })}
disabled={disableClear}
>
Clear
</Button>
<Button className="w-full lg:w-auto" onClick={handleFilter}>
Filter
</Button>
</div>
</div>
</div>
</Card>
);
};
Loading

0 comments on commit 9c56d5b

Please sign in to comment.