Skip to content

Commit

Permalink
feat(web): add multiple options to RollupFilter (#592)
Browse files Browse the repository at this point in the history
* fix(dayjs): do not return default value when trying to convert a date to daily date

* perf(api): optimize count retrieval using stats table for category, rollup, or timestamp filters only

Enhances performance by fetching counts directly from the stats table when counting elements with filters applied exclusively to 'category', 'rollup', or 'timestamp'.

* feat(web): add multiple prop to Dropdown

* feat(web): add multi select prop to RollupFilter

* fix(web): add check in RollupFilter to make none option unique

* fix(web): add a linear opacity gradient to RollupFilter overflow

* chore(web): update changeset

* fix(web): simplify Dropdown multiple props strategy

* fix(web): add horizontal scroll on Dropdown overflow

* chore(web): update changeset with API changes

* refactor(web): refactor Dropdown Option type prop

* refactor(web): remove not needed Dropdown prop

* fix(web): remove empty from addresses in filters

* refactor(web): Improve rollup badge (#603)

* fix(web): apply same size styles to Mode & Linea rollups

* feat(web): add Camp rollup icon & update styles

* refactor(web): improve RollupBadge colors

* feat(web): add Paradex rollup icon & update styles

* chore(web): update changeset

* chore(web): optimize rollup icon svgs

---------

Co-authored-by: PJColombo <[email protected]>

* fix(web): highlight dropdown options background only on hover + improve selected options styles

* fix(web): fix tx & blob count tests after adding multiple from addresses

* test: re-add missing test

* fix(api): add support for counting multiple rollups in `getCount` procedures

* refactor(api): use the `IN` clause for filtering when multiple addresses are provided

---------

Co-authored-by: PJColombo <[email protected]>
  • Loading branch information
xFJA and PJColombo authored Nov 8, 2024
1 parent eb868bd commit 76bd799
Show file tree
Hide file tree
Showing 28 changed files with 473 additions and 212 deletions.
5 changes: 5 additions & 0 deletions .changeset/famous-camels-kneel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@blobscan/web": patch
---

Added missing rollups icons and improved RollupBadges' styles
5 changes: 5 additions & 0 deletions .changeset/modern-humans-drum.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@blobscan/api": minor
---

Added the posibility to filter by multiple `from` addresses
5 changes: 5 additions & 0 deletions .changeset/old-pianos-help.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@blobscan/web": minor
---

Added multiple options to RollupFilter
1 change: 1 addition & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"react-loading-skeleton": "^3.3.1",
"react-tailwindcss-datepicker": "^1.6.6",
"superjson": "1.9.1",
"tailwind-gradient-mask-image": "^1.2.0",
"tailwind-merge": "^2.4.0",
"viem": "^2.17.4",
"zod": "^3.21.4"
Expand Down
19 changes: 11 additions & 8 deletions apps/web/src/components/Badges/RollupBadge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,18 @@ const ROLLUP_CONFIG: Record<Rollup, { style: string; label?: string }> = {
style: "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300",
},
blast: {
style: "bg-slate-100 text-slate-800 dark:bg-slate-900 dark:text-slate-50",
style:
"bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-50",
},
boba: {
style: "bg-slate-100 text-slate-800 dark:bg-slate-900 dark:text-slate-50",
},
camp: {
style: "bg-slate-100 text-slate-800 dark:bg-slate-900 dark:text-slate-50",
style:
"bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-50",
},
kroma: {
style: "bg-slate-100 text-slate-800 dark:bg-slate-900 dark:text-slate-50",
style: "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-50",
},
linea: {
style: "bg-slate-100 text-slate-800 dark:bg-slate-900 dark:text-slate-50",
Expand All @@ -36,14 +38,14 @@ const ROLLUP_CONFIG: Record<Rollup, { style: string; label?: string }> = {
},
optimism: {
style:
"bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-300",
"bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-100",
},
optopia: {
style: "bg-slate-100 text-slate-800 dark:bg-slate-900 dark:text-slate-50",
style: "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-100",
},
paradex: {
style:
"bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-300",
"bg-purple-200 text-purple-900 dark:bg-purple-900 dark:text-purple-300",
},
pgn: {
style: "bg-slate-100 text-slate-800 dark:bg-slate-900 dark:text-slate-50",
Expand All @@ -57,14 +59,15 @@ const ROLLUP_CONFIG: Record<Rollup, { style: string; label?: string }> = {
style: "bg-amber-100 text-slate-950 dark:bg-slate-900 dark:text-slate-50",
},
taiko: {
style: "bg-pink-100 text-pink-800 dark:bg-pink-900 dark:text-pink-300",
style: "bg-pink-100 text-pink-800 dark:bg-pink-900 dark:text-pink-100",
},
zksync: {
style: "bg-slate-200 text-slate-800 dark:bg-slate-700 dark:text-slate-300",
label: "zkSync",
},
zora: {
style: "bg-zinc-400 text-zinc-800 dark:bg-zinc-700 dark:text-zinc-300",
style:
"bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-300",
},
};

Expand Down
39 changes: 22 additions & 17 deletions apps/web/src/components/Dropdown/Option.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,33 @@
import { ListboxOption } from "@headlessui/react";
import { CheckIcon } from "@heroicons/react/24/outline";

import type { Option as OptionProps } from ".";
import type { Option as OptionType } from ".";

interface OptionProps {
option: OptionType;
}

export const Option: React.FC<OptionProps> = function (props) {
const { prefix, label, value } = props;
const { prefix, label, value } = props.option;
return (
<ListboxOption
className={({ focus }) =>
`relative cursor-pointer select-none px-4 py-2 ${
focus
? "bg-controlActive-light dark:bg-controlActive-dark dark:text-content-dark"
: "text-contentSecondary-light dark:text-contentSecondary-dark"
}`
}
value={props}
className={`relative cursor-pointer select-none px-4 py-2 text-contentSecondary-light hover:bg-controlActive-light data-[selected]:font-semibold data-[selected]:text-content-light dark:text-contentSecondary-dark hover:dark:bg-controlActive-dark data-[selected]:dark:font-semibold data-[selected]:dark:text-content-dark`}
value={props.option}
>
{({ selected }) => (
<div className="flex items-center gap-3">
{prefix && prefix}
<span
className={`block truncate text-sm ${selected ? "font-bold" : ""}`}
>
{label ? label : value}
</span>
<div className="flex items-center justify-between gap-3">
<div className="flex flex-row items-center gap-2">
{prefix && prefix}
<span className="block truncate text-sm">
{label ? label : value}
</span>
</div>
{selected && (
<CheckIcon
className="group pointer-events-none absolute right-2.5 top-2.5 size-4"
aria-hidden="true"
/>
)}
</div>
)}
</ListboxOption>
Expand Down
65 changes: 52 additions & 13 deletions apps/web/src/components/Dropdown/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Fragment, useRef } from "react";
import type { ReactNode } from "react";
import {
Listbox,
Expand All @@ -6,57 +7,95 @@ import {
Transition,
} from "@headlessui/react";
import { ChevronUpDownIcon, XMarkIcon } from "@heroicons/react/24/solid";
import cn from "classnames";

import useOverflow from "~/hooks/useOverflow";
import { Option } from "./Option";

export interface Option {
value: string | number;
label?: ReactNode;
prefix?: ReactNode;
selectedLabel?: ReactNode;
}

export interface DropdownProps {
options: Option[];
selected?: Option | null;
width?: string;
placeholder?: string;
clearable?: boolean;
onChange(newOption: Option | null): void;
selected?: Option | Option[] | null;
multiple?: boolean;
onChange(newOption: Option | Option[] | null): void;
}

const DEFAULT_WIDTH = "w-32";

export const Dropdown: React.FC<DropdownProps> = function ({
options,
selected,
multiple,
width,
onChange,
clearable = false,
placeholder = "Select an item",
}) {
const hasSelectedValue = Array.isArray(selected)
? selected.length > 0
: selected;

const containerRef = useRef<HTMLDivElement | null>(null);
const innerRef = useRef<HTMLDivElement | null>(null);
const isOverflowing = useOverflow(containerRef, innerRef);

return (
<Listbox value={selected} onChange={onChange}>
<Listbox value={selected} onChange={onChange} multiple={multiple}>
<div className="relative">
<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-8 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
) : (
<div className="text-hint-light dark:text-hint-dark">
{placeholder}
</div>
<div
className={cn(
{
"gradient-mask-r-90": isOverflowing,
},
"flex h-full items-center overflow-auto align-middle"
)}
ref={containerRef}
>
<div className="h-fit w-fit" ref={innerRef}>
{hasSelectedValue ? (
Array.isArray(selected) ? (
<div className="flex flex-row items-center gap-1">
{selected.map((s) => {
return (
<Fragment key={s.value}>
{s.selectedLabel ? s.selectedLabel : s.label}
</Fragment>
);
})}
</div>
) : selected?.label ? (
selected.label
) : (
selected?.value
)
) : (
<div className="text-hint-light dark:text-hint-dark">
{placeholder}
</div>
)}
</div>
</div>
<div className="absolute inset-y-0 right-0 flex items-center pr-2">
{clearable && selected ? (
{clearable && hasSelectedValue ? (
<XMarkIcon
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);
multiple ? onChange([]) : onChange(null);
}}
/>
) : (
Expand All @@ -74,7 +113,7 @@ export const Dropdown: React.FC<DropdownProps> = function ({
>
<ListboxOptions className="absolute z-50 mt-1 max-h-60 w-full overflow-auto rounded-md bg-controlBackground-light py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none dark:bg-controlBackground-dark sm:text-sm">
{options.map((option, id) => (
<Option {...option} key={id} />
<Option key={id} option={option} />
))}
</ListboxOptions>
</Transition>
Expand Down
58 changes: 44 additions & 14 deletions apps/web/src/components/Filters/RollupFilter.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
import { useRef } from "react";
import type { FC } from "react";

import { getChainRollups } from "@blobscan/rollups";

import { Dropdown } from "~/components/Dropdown";
import type { DropdownProps, Option } from "~/components/Dropdown";
import type { Option } from "~/components/Dropdown";
import { RollupIcon } from "~/components/RollupIcon";
import { env } from "~/env.mjs";
import type { Rollup } from "~/types";
import { capitalize, getChainIdByName } from "~/utils";
import { Badge } from "../Badges/Badge";
import { RollupBadge } from "../Badges/RollupBadge";

type RollupFilterProps = Pick<DropdownProps, "selected"> & {
onChange(newRollup: Option | null): void;
type RollupFilterProps = {
onChange(newRollups: Option[]): void;
selected: Option[] | null;
};

const chainId = getChainIdByName(env.NEXT_PUBLIC_NETWORK_NAME);
Expand All @@ -19,31 +23,57 @@ const rollups = chainId ? getChainRollups(chainId) : [];
export const ROLLUP_OPTIONS: Option[] = [
{
value: "null",
selectedLabel: <Badge size="sm">None</Badge>,
label: "None",
},
...rollups.map(([rollupAddress, rollupName]) => ({
value: rollupAddress,
label: (
<div className="flex items-center gap-2">
<RollupIcon rollup={rollupName.toLowerCase() as Rollup} />
{capitalize(rollupName)}
</div>
),
})),
...rollups.map(([rollupAddress, rollupName]) => {
return {
value: rollupAddress,
selectedLabel: (
<RollupBadge rollup={rollupName.toLowerCase() as Rollup} size="sm" />
),
prefix: <RollupIcon rollup={rollupName.toLowerCase() as Rollup} />,
label: capitalize(rollupName),
};
}),
];

export const RollupFilter: FC<RollupFilterProps> = function ({
onChange,
selected,
}) {
const noneIsSelected = useRef<boolean>(false);

const handleOnChange = (newRollups_: Option[]) => {
let newRollups = newRollups_;
const noneOptionIndex = newRollups.findIndex((r) => r.value === "null");

if (noneIsSelected.current && newRollups.length > 1) {
noneIsSelected.current = false;
newRollups = newRollups.filter((_, index) => index !== noneOptionIndex);
}

if (
!noneIsSelected.current &&
noneOptionIndex !== -1 &&
newRollups.length > 1
) {
noneIsSelected.current = true;
newRollups = newRollups.filter((_, index) => index === noneOptionIndex);
}

onChange(newRollups);
};

return (
<Dropdown
selected={selected}
options={ROLLUP_OPTIONS}
onChange={onChange}
onChange={handleOnChange}
placeholder="Rollup"
width="sm:w-[130px] w-full md:max-lg:w-full"
width="sm:w-[130px] w-full xl:w-[240px] md:max-lg:w-full"
clearable
multiple
/>
);
};
Loading

0 comments on commit 76bd799

Please sign in to comment.