Skip to content

Commit

Permalink
Use getOptionIcon and getOptionLabel to better support unknown values
Browse files Browse the repository at this point in the history
  • Loading branch information
TWilson023 committed Jun 3, 2024
1 parent 48f891d commit 0e831af
Show file tree
Hide file tree
Showing 4 changed files with 126 additions and 56 deletions.
103 changes: 68 additions & 35 deletions apps/web/ui/analytics/toggle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
} from "@/lib/analytics/constants";
import { validDateRangeForPlan } from "@/lib/analytics/utils";
import useDomains from "@/lib/swr/use-domains";
import useLinks from "@/lib/swr/use-links";
import useTags from "@/lib/swr/use-tags";
import useWorkspace from "@/lib/swr/use-workspace";
import { LinkProps } from "@/lib/types";
Expand Down Expand Up @@ -45,7 +46,13 @@ import {
} from "@dub/utils";
import va from "@vercel/analytics";
import { readStreamableValue } from "ai/rsc";
import { useCallback, useContext, useMemo, useState } from "react";
import {
ComponentProps,
useCallback,
useContext,
useMemo,
useState,
} from "react";
import { AnalyticsContext } from ".";
import LinkLogo from "../links/link-logo";
import { COLORS_LIST } from "../links/tag-badge";
Expand All @@ -66,6 +73,7 @@ export default function Toggle() {

const { tags } = useTags();
const { allDomains: domains } = useDomains();
const { links: allLinks } = useLinks();

const [requestedFilters, setRequestedFilters] = useState<string[]>([]);

Expand Down Expand Up @@ -115,7 +123,7 @@ export default function Toggle() {

const [streaming, setStreaming] = useState<boolean>(false);

const filters = useMemo(
const filters: ComponentProps<typeof Filter.Select>["filters"] = useMemo(
() => [
...(isPublicStatsPage
? []
Expand All @@ -142,24 +150,40 @@ export default function Toggle() {
key: "domain",
icon: Globe,
label: "Domain",
getOptionIcon: (value) => (
<BlurImage
src={`${GOOGLE_FAVICON_URL}${value}`}
alt={value}
className="h-4 w-4 rounded-full"
width={16}
height={16}
/>
),
options: domains.map((domain) => ({
value: domain.slug,
label: domain.slug,
icon: (
<BlurImage
src={`${GOOGLE_FAVICON_URL}${domain.slug}`}
alt={domain.slug}
className="h-4 w-4 rounded-full"
width={16}
height={16}
/>
),
})),
},
{
key: "link",
icon: Hyperlink,
label: "Link",
getOptionIcon: (value, { option }) => {
const url =
option?.data?.url ??
allLinks?.find(
({ domain, key }) =>
value.includes(key) &&
linkConstructor({ domain, key }) === value,
)?.url;

return url ? (
<LinkLogo
apexDomain={getApexDomain(url)}
className="h-4 w-4 sm:h-4 sm:w-4"
/>
) : null;
},
options:
links?.map(
({
Expand All @@ -170,20 +194,29 @@ export default function Toggle() {
}: LinkProps & { count?: number }) => ({
value: linkConstructor({ domain, key }),
label: linkConstructor({ domain, key, pretty: true }),
icon: (
<LinkLogo
apexDomain={getApexDomain(url)}
className="h-4 w-4 sm:h-4 sm:w-4"
/>
),
right: nFormatter(count, { full: true }),
data: { url },
}),
) ?? null,
},
{
key: "tagId",
icon: Tag,
label: "Tag",
getOptionIcon: (value, { option }) => {
const tag =
option?.data?.color ?? tags?.find(({ id }) => id === value);
return tag ? (
<div
className={cn(
"rounded-md p-1.5",
COLORS_LIST.find(({ color }) => color === tag.color)?.css,
)}
>
<Tag className="h-2.5 w-2.5" />
</div>
) : null;
},
options:
tags?.map((tag) => ({
value: tag.id,
Expand All @@ -199,6 +232,7 @@ export default function Toggle() {
</div>
),
label: tag.name,
data: { color: tag.color },
})) ?? null,
},
]),
Expand All @@ -223,17 +257,18 @@ export default function Toggle() {
key: "country",
icon: FlagWavy,
label: "Country",
getOptionIcon: (value) => (
<img
alt={value}
src={`https://flag.vercel.app/m/${value}.svg`}
className="h-2.5 w-4"
/>
),
getOptionLabel: (value) => COUNTRIES[value],
options:
countries?.map(({ country, count }) => ({
value: country,
label: COUNTRIES[country],
icon: (
<img
alt={country}
src={`https://flag.vercel.app/m/${country}.svg`}
className="h-2.5 w-4"
/>
),
right: nFormatter(count, { full: true }),
})) ?? null,
},
Expand All @@ -259,43 +294,41 @@ export default function Toggle() {
key: "device",
icon: MobilePhone,
label: "Device",
getOptionIcon: (value) => (
<DeviceIcon display={value} tab="devices" className="h-4 w-4" />
),
options:
devices?.map(({ device, count }) => ({
value: device,
label: device,
icon: (
<DeviceIcon display={device} tab="devices" className="h-4 w-4" />
),
right: nFormatter(count, { full: true }),
})) ?? null,
},
{
key: "browser",
icon: Window,
label: "Browser",
getOptionIcon: (value) => (
<DeviceIcon display={value} tab="browsers" className="h-4 w-4" />
),
options:
browsers?.map(({ browser, count }) => ({
value: browser,
label: browser,
icon: (
<DeviceIcon
display={browser}
tab="browsers"
className="h-4 w-4"
/>
),
right: nFormatter(count, { full: true }),
})) ?? null,
},
{
key: "os",
icon: Cube,
label: "OS",
getOptionIcon: (value) => (
<DeviceIcon display={value} tab="os" className="h-4 w-4" />
),
options:
os?.map(({ os, count }) => ({
value: os,
label: os,
icon: <DeviceIcon display={os} tab="os" className="h-4 w-4" />,
right: nFormatter(count, { full: true }),
})) ?? null,
},
Expand Down
37 changes: 23 additions & 14 deletions packages/ui/src/filter/filter-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,19 @@ export function FilterList({
: o.value === value,
);

const OptionIcon =
option?.icon ??
filter.getOptionIcon?.(value, {
key: filter.key,
option,
}) ??
filter.icon;

const optionLabel =
option?.label ??
filter.getOptionLabel?.(value, { key: filter.key, option }) ??
value;

return (
<motion.div
key={key}
Expand All @@ -82,20 +95,16 @@ export function FilterList({
{/* Option */}
<div className="flex items-center gap-2.5 px-3 py-2">
{filter.options ? (
option ? (
<>
<span className="shrink-0 text-gray-600">
{isReactNode(option.icon) ? (
option.icon
) : (
<option.icon className="h-4 w-4" />
)}
</span>
{truncate(option.label, 30)}
</>
) : (
value
)
<>
<span className="shrink-0 text-gray-600">
{isReactNode(OptionIcon) ? (
OptionIcon
) : (
<OptionIcon className="h-4 w-4" />
)}
</span>
{truncate(optionLabel, 30)}
</>
) : (
<div className="h-5 w-12 animate-pulse rounded-md bg-gray-200" />
)}
Expand Down
24 changes: 19 additions & 5 deletions packages/ui/src/filter/filter-select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -155,8 +155,8 @@ export function FilterSelect({
filters.map((filter) => (
<>
<FilterButton
{...filter}
key={filter.key}
filter={filter}
onSelect={() => openFilter(filter.key)}
/>
{filter.separatorAfter && (
Expand All @@ -173,8 +173,9 @@ export function FilterSelect({

return (
<FilterButton
{...option}
key={option.value}
filter={selectedFilter}
option={option}
right={
isSelected ? (
<Check className="h-4 w-4" />
Expand Down Expand Up @@ -300,14 +301,27 @@ const FilterScroll = forwardRef(
);

function FilterButton({
icon: Icon,
label,
filter,
option,
right,
onSelect,
}: Omit<Filter | FilterOption, "key" | "value"> & {
}: {
filter: Filter;
option?: FilterOption;
right?: ReactNode;
onSelect: () => void;
}) {
const Icon = option
? option.icon ??
filter.getOptionIcon?.(option.value, { key: filter.key, option }) ??
filter.icon
: filter.icon;

const label = option
? option.label ??
filter.getOptionLabel?.(option.value, { key: filter.key, option })
: filter.label;

return (
<Command.Item
className={cn(
Expand Down
18 changes: 16 additions & 2 deletions packages/ui/src/filter/types.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,31 @@
import { LucideIcon } from "lucide-react";
import { ComponentType, ReactNode, SVGProps } from "react";

type FilterIcon =
| LucideIcon
| ReactNode
| ComponentType<SVGProps<SVGSVGElement>>;

export type Filter = {
key: string;
icon: LucideIcon | ReactNode | ComponentType<SVGProps<SVGSVGElement>>;
icon: FilterIcon;
label: string;
separatorAfter?: boolean;
options: FilterOption[] | null;
getOptionIcon?: (
value: FilterOption["value"],
props: { key: Filter["key"]; option?: FilterOption },
) => FilterIcon | null;
getOptionLabel?: (
value: FilterOption["value"],
props: { key: Filter["key"]; option?: FilterOption },
) => string | null;
};

export type FilterOption = {
value: any;
icon: LucideIcon | ReactNode | ComponentType<SVGProps<SVGSVGElement>>;
label: string;
right?: ReactNode;
icon?: FilterIcon;
data?: Record<string, any>;
};

0 comments on commit 0e831af

Please sign in to comment.