Skip to content

Commit

Permalink
feat(dashboard): add website quick switch dropdown (#93)
Browse files Browse the repository at this point in the history
* refactor: move dateselector to more generic component

* feat: add quick switcher

* fix: make responsive

* style: clean up imports

* fix: use hostname param for default state

* fix: remove console log
  • Loading branch information
ayuhito authored Jul 9, 2024
1 parent 9958b7d commit 5d3363f
Show file tree
Hide file tree
Showing 8 changed files with 208 additions and 129 deletions.
1 change: 0 additions & 1 deletion .mise.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
[tools]
zig = "0.13"
go = "1.22"
bun = "latest"
node = "20"
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
.target {
min-width: 192px;
height: 40px;
padding-left: 32px;
padding-left: 16px;
color: var(--me-color-text-light);
background-color: var(--me-color-bg-grey-blue);
border: 1px solid var(--me-color-bg-grey-blue);
Expand All @@ -14,12 +14,16 @@
&:focus-visible {
outline: 2px solid var(--mantine-color-blue-outline);
}

&[data-left="true"] {
padding-left: 32px;
}
}

.targetWrapper {
@media (max-width: $mantine-breakpoint-xs) {
width: 100%;
margin: 4px 0 16px 0;
margin: 4px 0;
}
}

Expand Down
126 changes: 126 additions & 0 deletions dashboard/app/components/DropdownSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { Combobox, InputBase, useCombobox } from '@mantine/core';
import { useDidUpdate } from '@mantine/hooks';
import { useNavigate, useSearchParams } from '@remix-run/react';
import { useCallback, useMemo, useState } from 'react';

import classes from './DropdownSelect.module.css';

interface DropdownSelectBase {
defaultValue: string;
defaultLabel: string;
selectAriaLabel: string;
records: Record<string, string>;
groupEndValues?: string[];
leftSection?: React.ReactNode;
}

interface DropdownSearchParams extends DropdownSelectBase {
type: 'searchParams';
// Key to update and read from search params
searchParamKey: string;
}

interface DropdownSelectLink extends DropdownSelectBase {
type: 'link';
}

type DropdownSelectProps = DropdownSearchParams | DropdownSelectLink;

const isSearchParams = (
props: DropdownSelectProps,
): props is DropdownSearchParams => props.type === 'searchParams';

export const DropdownSelect = (props: DropdownSelectProps) => {
const {
defaultLabel,
defaultValue,
selectAriaLabel,
records,
groupEndValues = [],
leftSection,
} = props;

const [searchParams, setSearchParams] = useSearchParams();
const navigate = useNavigate();
const [option, setOption] = useState<string>(
isSearchParams(props)
? searchParams.get(props.searchParamKey) ?? defaultValue
: defaultValue,
);

const combobox = useCombobox({
onDropdownClose: () => combobox.resetSelectedOption(),
onDropdownOpen: (eventSource) => {
eventSource === 'keyboard'
? combobox.selectActiveOption()
: combobox.updateSelectedOptionIndex('active');
},
});

useDidUpdate(() => {
if (isSearchParams(props)) {
setSearchParams((prevParams) => {
const newParams = new URLSearchParams(prevParams);
newParams.set(props.searchParamKey, option);
return newParams;
});
} else {
navigate(`/${option}`, { relative: 'route' });
}
}, [option]);

const handleOptionSubmit = useCallback(
(value: string) => {
setOption(value);
combobox.toggleDropdown();
},
[combobox.toggleDropdown],
);

const options = useMemo(
() =>
Object.entries(records).map(([value, label]) => (
<Combobox.Option
key={value}
value={value}
active={value === option}
data-group-end={groupEndValues.includes(value)}
role="option"
aria-selected={value === option}
>
{label}
</Combobox.Option>
)),
[records, groupEndValues, option],
);

return (
<Combobox
classNames={{ dropdown: classes.dropdown, option: classes.option }}
store={combobox}
resetSelectionOnOptionHover
onOptionSubmit={handleOptionSubmit}
>
<Combobox.Target>
<InputBase
classNames={{ input: classes.target }}
className={classes.targetWrapper}
component="button"
type="button"
pointer
rightSection={<Combobox.Chevron />}
rightSectionPointerEvents="none"
onClick={() => combobox.toggleDropdown()}
aria-label={selectAriaLabel}
leftSection={leftSection}
data-left={Boolean(leftSection)}
>
{records[option] ?? defaultLabel}
</InputBase>
</Combobox.Target>
<Combobox.Dropdown>
<Combobox.Options>{options}</Combobox.Options>
</Combobox.Dropdown>
</Combobox>
);
};
114 changes: 0 additions & 114 deletions dashboard/app/components/stats/DateSelector.tsx

This file was deleted.

9 changes: 9 additions & 0 deletions dashboard/app/components/stats/StatsHeader.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@
}
}

.dropdowns {
align-items: center;

@media (max-width: $mantine-breakpoint-xs) {
gap: 8px;
width: 100%;
}
}

/* Chart Toggle */
.toggle {
padding: 4px;
Expand Down
54 changes: 51 additions & 3 deletions dashboard/app/components/stats/StatsHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,18 @@ import {
Tooltip,
UnstyledButton,
} from '@mantine/core';
import { useMediaQuery } from '@mantine/hooks';
import { useParams } from '@remix-run/react';
import { useState } from 'react';
import { ScrollContainer } from 'react-indiana-drag-scroll';

import { DropdownSelect } from '@/components/DropdownSelect';
import { IconAreaChart } from '@/components/icons/area';
import { IconBarChart } from '@/components/icons/bar';
import { IconCalendar } from '@/components/icons/calendar';
import { InnerHeader } from '@/components/layout/InnerHeader';
import { useChartType } from '@/hooks/use-chart-type';

import { DateComboBox } from './DateSelector';
import { HeaderDataBox } from './HeaderDataBox';
import type { ChartType, StatHeaderData } from './types';

Expand All @@ -22,8 +25,26 @@ import classes from './StatsHeader.module.css';
interface StatsHeaderProps {
stats: StatHeaderData[];
chart: string;
websites: string[];
}

const DATE_PRESETS = {
today: 'Today',
yesterday: 'Yesterday',
'12h': 'Previous 12 hours',
'24h': 'Previous 24 hours',
'72h': 'Previous 72 hours',
'7d': 'Previous 7 days',
'14d': 'Previous 14 days',
'30d': 'Previous 30 days',
quarter: 'Previous quarter',
half: 'Previous half year',
year: 'Previous year',
all: 'All time',
} as const;

const DATE_GROUP_END_VALUES = ['yesterday', '30d', 'year'];

const CHART_TYPES = [
{
label: 'Toggle area chart',
Expand Down Expand Up @@ -84,12 +105,39 @@ const SegmentedChartControl = () => {
);
};

const StatsHeader = ({ stats, chart }: StatsHeaderProps) => {
const StatsHeader = ({ stats, chart, websites }: StatsHeaderProps) => {
const { hostname } = useParams();
const hideWebsiteSelector = useMediaQuery('(36em < width < 62em)');
// Convert websites array to object with same key-val for DropdownSelect
const websitesRecord = Object.fromEntries(
websites.map((website) => [website, website]),
);

return (
<InnerHeader>
<Flex className={classes.title}>
<h1>Dashboard</h1>
<DateComboBox />
<Group className={classes.dropdowns}>
{!hideWebsiteSelector && (
<DropdownSelect
records={websitesRecord}
defaultValue={hostname ?? ''}
defaultLabel="Unknown"
selectAriaLabel="Select website"
type="link"
/>
)}
<DropdownSelect
records={DATE_PRESETS}
defaultValue="today"
defaultLabel="Custom range"
selectAriaLabel="Select date range"
groupEndValues={DATE_GROUP_END_VALUES}
leftSection={<IconCalendar />}
type="searchParams"
searchParamKey="period"
/>
</Group>
</Flex>
<ScrollContainer>
<Group justify="space-between" align="flex-end" mt="xs">
Expand Down
Loading

0 comments on commit 5d3363f

Please sign in to comment.