Added tables to compare screen.

This commit is contained in:
Mike Cao 2024-05-25 09:31:38 -07:00
parent 154b559315
commit 13e11ee371
15 changed files with 199 additions and 66 deletions

View File

@ -9,7 +9,7 @@ export function WebsiteChart({
compareMode = false, compareMode = false,
}: { }: {
websiteId: string; websiteId: string;
compareMode: boolean; compareMode?: boolean;
}) { }) {
const { dateRange, dateCompare } = useDateRange(websiteId); const { dateRange, dateCompare } = useDateRange(websiteId);
const { startDate, endDate, unit } = dateRange; const { startDate, endDate, unit } = dateRange;
@ -27,13 +27,13 @@ export function WebsiteChart({
result['compare'] = { result['compare'] = {
pageviews: result.pageviews.map(({ x }, i) => ({ pageviews: result.pageviews.map(({ x }, i) => ({
x, x,
y: compare.pageviews[i].y, y: compare.pageviews[i]?.y,
d: compare.pageviews[i].x, d: compare.pageviews[i]?.x,
})), })),
sessions: result.sessions.map(({ x }, i) => ({ sessions: result.sessions.map(({ x }, i) => ({
x, x,
y: compare.sessions[i].y, y: compare.sessions[i]?.y,
d: compare.pageviews[i].x, d: compare.sessions[i]?.x,
})), })),
}; };
} }

View File

@ -1,25 +1,19 @@
'use client'; 'use client';
import { Loading } from 'react-basics';
import { usePathname } from 'next/navigation'; import { usePathname } from 'next/navigation';
import Page from 'components/layout/Page';
import FilterTags from 'components/metrics/FilterTags'; import FilterTags from 'components/metrics/FilterTags';
import { useNavigation, useWebsite } from 'components/hooks'; import { useNavigation } from 'components/hooks';
import WebsiteChart from './WebsiteChart'; import WebsiteChart from './WebsiteChart';
import WebsiteExpandedView from './WebsiteExpandedView'; import WebsiteExpandedView from './WebsiteExpandedView';
import WebsiteHeader from './WebsiteHeader'; import WebsiteHeader from './WebsiteHeader';
import WebsiteMetricsBar from './WebsiteMetricsBar'; import WebsiteMetricsBar from './WebsiteMetricsBar';
import WebsiteTableView from './WebsiteTableView'; import WebsiteTableView from './WebsiteTableView';
import WebsiteProvider from './WebsiteProvider';
import { FILTER_COLUMNS } from 'lib/constants'; import { FILTER_COLUMNS } from 'lib/constants';
export default function WebsiteDetails({ websiteId }: { websiteId: string }) { export default function WebsiteDetailsPage({ websiteId }: { websiteId: string }) {
const { data: website, isLoading, error } = useWebsite(websiteId);
const pathname = usePathname(); const pathname = usePathname();
const { query } = useNavigation(); const { query } = useNavigation();
if (isLoading || error) {
return <Page isLoading={isLoading} error={error} />;
}
const showLinks = !pathname.includes('/share/'); const showLinks = !pathname.includes('/share/');
const { view } = query; const { view } = query;
@ -31,18 +25,13 @@ export default function WebsiteDetails({ websiteId }: { websiteId: string }) {
}, {}); }, {});
return ( return (
<> <WebsiteProvider websiteId={websiteId}>
<WebsiteHeader websiteId={websiteId} showLinks={showLinks} /> <WebsiteHeader websiteId={websiteId} showLinks={showLinks} />
<FilterTags websiteId={websiteId} params={params} /> <FilterTags websiteId={websiteId} params={params} />
<WebsiteMetricsBar websiteId={websiteId} showChange={true} sticky={true} /> <WebsiteMetricsBar websiteId={websiteId} showChange={true} sticky={true} />
<WebsiteChart websiteId={websiteId} /> <WebsiteChart websiteId={websiteId} />
{!website && <Loading icon="dots" style={{ minHeight: 300 }} />} {!view && <WebsiteTableView websiteId={websiteId} />}
{website && ( {view && <WebsiteExpandedView websiteId={websiteId} />}
<> </WebsiteProvider>
{!view && <WebsiteTableView websiteId={websiteId} domainName={website.domain} />}
{view && <WebsiteExpandedView websiteId={websiteId} domainName={website.domain} />}
</>
)}
</>
); );
} }

View File

@ -10,17 +10,10 @@ import CountriesTable from 'components/metrics/CountriesTable';
import EventsTable from 'components/metrics/EventsTable'; import EventsTable from 'components/metrics/EventsTable';
import EventsChart from 'components/metrics/EventsChart'; import EventsChart from 'components/metrics/EventsChart';
export default function WebsiteTableView({ export default function WebsiteTableView({ websiteId }: { websiteId: string }) {
websiteId,
domainName,
}: {
websiteId: string;
domainName: string;
}) {
const [countryData, setCountryData] = useState(); const [countryData, setCountryData] = useState();
const tableProps = { const tableProps = {
websiteId, websiteId,
domainName,
limit: 10, limit: 10,
}; };

View File

@ -5,6 +5,8 @@ import FilterTags from 'components/metrics/FilterTags';
import { useNavigation } from 'components/hooks'; import { useNavigation } from 'components/hooks';
import { FILTER_COLUMNS } from 'lib/constants'; import { FILTER_COLUMNS } from 'lib/constants';
import WebsiteChart from '../WebsiteChart'; import WebsiteChart from '../WebsiteChart';
import WebsiteCompareTables from './WebsiteCompareTables';
import WebsiteProvider from '../WebsiteProvider';
export function WebsiteComparePage({ websiteId }) { export function WebsiteComparePage({ websiteId }) {
const { query } = useNavigation(); const { query } = useNavigation();
@ -17,12 +19,13 @@ export function WebsiteComparePage({ websiteId }) {
}, {}); }, {});
return ( return (
<> <WebsiteProvider websiteId={websiteId}>
<WebsiteHeader websiteId={websiteId} /> <WebsiteHeader websiteId={websiteId} />
<FilterTags websiteId={websiteId} params={params} /> <FilterTags websiteId={websiteId} params={params} />
<WebsiteMetricsBar websiteId={websiteId} compareMode={true} /> <WebsiteMetricsBar websiteId={websiteId} compareMode={true} />
<WebsiteChart websiteId={websiteId} compareMode={true} /> <WebsiteChart websiteId={websiteId} compareMode={true} />
</> <WebsiteCompareTables websiteId={websiteId} />
</WebsiteProvider>
); );
} }

View File

@ -0,0 +1,7 @@
.container {
margin-bottom: 60px;
}
.nav {
width: 200px;
}

View File

@ -0,0 +1,137 @@
import SideNav from 'components/layout/SideNav';
import { useMessages, useNavigation } from 'components/hooks';
import PagesTable from 'components/metrics/PagesTable';
import ReferrersTable from 'components/metrics/ReferrersTable';
import BrowsersTable from 'components/metrics/BrowsersTable';
import OSTable from 'components/metrics/OSTable';
import DevicesTable from 'components/metrics/DevicesTable';
import ScreenTable from 'components/metrics/ScreenTable';
import CountriesTable from 'components/metrics/CountriesTable';
import RegionsTable from 'components/metrics/RegionsTable';
import CitiesTable from 'components/metrics/CitiesTable';
import LanguagesTable from 'components/metrics/LanguagesTable';
import EventsTable from 'components/metrics/EventsTable';
import QueryParametersTable from 'components/metrics/QueryParametersTable';
import { Grid, GridRow } from 'components/layout/Grid';
import styles from './WebsiteCompareTables.module.css';
import { useContext, useState } from 'react';
import MetricsTable from 'components/metrics/MetricsTable';
import FilterLink from 'components/common/FilterLink';
import { WebsiteContext } from '../WebsiteProvider';
const views = {
url: PagesTable,
title: PagesTable,
referrer: ReferrersTable,
browser: BrowsersTable,
os: OSTable,
device: DevicesTable,
screen: ScreenTable,
country: CountriesTable,
region: RegionsTable,
city: CitiesTable,
language: LanguagesTable,
event: EventsTable,
query: QueryParametersTable,
};
export function WebsiteCompareTables({ websiteId }: { websiteId: string }) {
const { domain } = useContext(WebsiteContext);
const [data, setData] = useState([]);
const { formatMessage, labels } = useMessages();
const {
renderUrl,
query: { view },
} = useNavigation();
const Component: typeof MetricsTable = views[view] || (() => null);
const items = [
{
key: 'url',
label: formatMessage(labels.pages),
url: renderUrl({ view: 'url' }),
},
{
key: 'referrer',
label: formatMessage(labels.referrers),
url: renderUrl({ view: 'referrer' }),
},
{
key: 'browser',
label: formatMessage(labels.browsers),
url: renderUrl({ view: 'browser' }),
},
{
key: 'os',
label: formatMessage(labels.os),
url: renderUrl({ view: 'os' }),
},
{
key: 'device',
label: formatMessage(labels.devices),
url: renderUrl({ view: 'device' }),
},
{
key: 'country',
label: formatMessage(labels.countries),
url: renderUrl({ view: 'country' }),
},
{
key: 'region',
label: formatMessage(labels.regions),
url: renderUrl({ view: 'region' }),
},
{
key: 'city',
label: formatMessage(labels.cities),
url: renderUrl({ view: 'city' }),
},
{
key: 'language',
label: formatMessage(labels.languages),
url: renderUrl({ view: 'language' }),
},
{
key: 'screen',
label: formatMessage(labels.screens),
url: renderUrl({ view: 'screen' }),
},
{
key: 'event',
label: formatMessage(labels.events),
url: renderUrl({ view: 'event' }),
},
{
key: 'query',
label: formatMessage(labels.queryParameters),
url: renderUrl({ view: 'query' }),
},
];
const renderLabel = ({ x, y }, index) => {
return (
<FilterLink
id={view}
value={x}
label={!x && formatMessage(labels.none)}
externalUrl={
view === 'url' ? `${domain.startsWith('http') ? domain : `https://${domain}`}${x}` : null
}
>
{y} : {data[index]?.y} !
</FilterLink>
);
};
return (
<Grid className={styles.container}>
<GridRow columns="compare">
<SideNav className={styles.nav} items={items} selectedKey={view} shallow={true} />
<Component websiteId={websiteId} limit={20} showMore={false} onDataLoad={setData} />
<Component websiteId={websiteId} limit={20} showMore={false} renderLabel={renderLabel} />
</GridRow>
</Grid>
);
}
export default WebsiteCompareTables;

View File

@ -1,8 +1,8 @@
import WebsiteDetails from './WebsiteDetails'; import WebsiteDetailsPage from './WebsiteDetailsPage';
import { Metadata } from 'next'; import { Metadata } from 'next';
export default function WebsitePage({ params: { websiteId } }) { export default function WebsitePage({ params: { websiteId } }) {
return <WebsiteDetails websiteId={websiteId} />; return <WebsiteDetailsPage websiteId={websiteId} />;
} }
export const metadata: Metadata = { export const metadata: Metadata = {

View File

@ -1,5 +1,5 @@
'use client'; 'use client';
import WebsiteDetails from 'app/(main)/websites/[websiteId]/WebsiteDetails'; import WebsiteDetailsPage from '../../(main)/websites/[websiteId]/WebsiteDetailsPage';
import { useShareToken } from 'components/hooks'; import { useShareToken } from 'components/hooks';
import Page from 'components/layout/Page'; import Page from 'components/layout/Page';
import Header from './Header'; import Header from './Header';
@ -17,7 +17,7 @@ export default function SharePage({ shareId }) {
<div className={styles.container}> <div className={styles.container}>
<Page> <Page>
<Header /> <Header />
<WebsiteDetails websiteId={shareToken.websiteId} /> <WebsiteDetailsPage websiteId={shareToken.websiteId} />
<Footer /> <Footer />
</Page> </Page>
</div> </div>

View File

@ -98,9 +98,9 @@ export function Chart({
// Allow config changes before update // Allow config changes before update
onUpdate?.(chart.current); onUpdate?.(chart.current);
setLegendItems(chart.current.legend.legendItems);
chart.current.update(updateMode); chart.current.update(updateMode);
setLegendItems(chart.current.legend.legendItems);
}; };
useEffect(() => { useEffect(() => {

View File

@ -23,6 +23,15 @@ export function WebsiteDateFilter({ websiteId }: { websiteId: string }) {
return ( return (
<div className={styles.container}> <div className={styles.container}>
<DateFilter
className={styles.dropdown}
value={value}
startDate={startDate}
endDate={endDate}
offset={offset}
onChange={handleChange}
showAllTime={true}
/>
{value !== 'all' && !value.startsWith('range') && ( {value !== 'all' && !value.startsWith('range') && (
<div className={styles.buttons}> <div className={styles.buttons}>
<Button onClick={() => handleIncrement(-1)}> <Button onClick={() => handleIncrement(-1)}>
@ -37,15 +46,6 @@ export function WebsiteDateFilter({ websiteId }: { websiteId: string }) {
</Button> </Button>
</div> </div>
)} )}
<DateFilter
className={styles.dropdown}
value={value}
startDate={startDate}
endDate={endDate}
offset={offset}
onChange={handleChange}
showAllTime={true}
/>
</div> </div>
); );
} }

View File

@ -8,6 +8,10 @@
border-top: 1px solid var(--base300); border-top: 1px solid var(--base300);
} }
.row.compare {
grid-template-columns: max-content 1fr 1fr;
}
.col { .col {
padding: 20px; padding: 20px;
min-height: 430px; min-height: 430px;

View File

@ -1,6 +1,7 @@
import { CSSProperties } from 'react'; import { CSSProperties } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { mapChildren } from 'react-basics'; import { mapChildren } from 'react-basics';
// eslint-disable-next-line css-modules/no-unused-class
import styles from './Grid.module.css'; import styles from './Grid.module.css';
export interface GridProps { export interface GridProps {
@ -19,13 +20,13 @@ export function Grid({ className, style, children }: GridProps) {
export function GridRow(props: { export function GridRow(props: {
[x: string]: any; [x: string]: any;
columns?: 'one' | 'two' | 'three' | 'one-two' | 'two-one'; columns?: 'one' | 'two' | 'three' | 'one-two' | 'two-one' | 'compare';
className?: string; className?: string;
children?: any; children?: any;
}) { }) {
const { columns = 'two', className, children, ...otherProps } = props; const { columns = 'two', className, children, ...otherProps } = props;
return ( return (
<div {...otherProps} className={classNames(styles.row, className)}> <div {...otherProps} className={classNames(styles.row, className, { [styles[columns]]: true })}>
{mapChildren(children, child => { {mapChildren(children, child => {
return <div className={classNames(styles.col, { [styles[columns]]: true })}>{child}</div>; return <div className={classNames(styles.col, { [styles[columns]]: true })}>{child}</div>;
})} })}

View File

@ -14,7 +14,7 @@ export interface ListTableProps {
title?: string; title?: string;
metric?: string; metric?: string;
className?: string; className?: string;
renderLabel?: (row: any) => ReactNode; renderLabel?: (row: any, index: number) => ReactNode;
animate?: boolean; animate?: boolean;
virtualize?: boolean; virtualize?: boolean;
showPercentage?: boolean; showPercentage?: boolean;
@ -34,13 +34,13 @@ export function ListTable({
}: ListTableProps) { }: ListTableProps) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const getRow = row => { const getRow = (row: { x: any; y: any; z: any }, index: number) => {
const { x: label, y: value, z: percent } = row; const { x: label, y: value, z: percent } = row;
return ( return (
<AnimatedRow <AnimatedRow
key={label} key={label}
label={renderLabel ? renderLabel(row) : label ?? formatMessage(labels.unknown)} label={renderLabel ? renderLabel(row, index) : label ?? formatMessage(labels.unknown)}
value={value} value={value}
percent={percent} percent={percent}
animate={animate && !virtualize} animate={animate && !virtualize}
@ -50,7 +50,7 @@ export function ListTable({
}; };
const Row = ({ index, style }) => { const Row = ({ index, style }) => {
return <div style={style}>{getRow(data[index])}</div>; return <div style={style}>{getRow(data[index], index)}</div>;
}; };
return ( return (
@ -71,7 +71,7 @@ export function ListTable({
{Row} {Row}
</FixedSizeList> </FixedSizeList>
) : ( ) : (
data.map(row => getRow(row)) data.map(getRow)
)} )}
</div> </div>
</div> </div>
@ -97,9 +97,7 @@ const AnimatedRow = ({ label, value = 0, percent, animate, showPercentage = true
{showPercentage && ( {showPercentage && (
<div className={styles.percent}> <div className={styles.percent}>
<animated.div className={styles.bar} style={{ width: props.width.to(n => `${n}%`) }} /> <animated.div className={styles.bar} style={{ width: props.width.to(n => `${n}%`) }} />
<animated.span className={styles.percentValue}> <animated.span>{props.width.to(n => `${n?.toFixed?.(0)}%`)}</animated.span>
{props.width.to(n => `${n?.toFixed?.(0)}%`)}
</animated.span>
</div> </div>
)} )}
</div> </div>

View File

@ -18,7 +18,6 @@ import styles from './MetricsTable.module.css';
export interface MetricsTableProps extends ListTableProps { export interface MetricsTableProps extends ListTableProps {
websiteId: string; websiteId: string;
domainName: string;
type?: string; type?: string;
className?: string; className?: string;
dataFilter?: (data: any) => any; dataFilter?: (data: any) => any;
@ -27,6 +26,7 @@ export interface MetricsTableProps extends ListTableProps {
onDataLoad?: (data: any) => void; onDataLoad?: (data: any) => void;
onSearch?: (search: string) => void; onSearch?: (search: string) => void;
allowSearch?: boolean; allowSearch?: boolean;
showMore?: boolean;
children?: ReactNode; children?: ReactNode;
} }
@ -39,6 +39,7 @@ export function MetricsTable({
onDataLoad, onDataLoad,
delay = null, delay = null,
allowSearch = false, allowSearch = false,
showMore = true,
children, children,
...props ...props
}: MetricsTableProps) { }: MetricsTableProps) {
@ -98,7 +99,7 @@ export function MetricsTable({
)} )}
{!data && isLoading && !isFetched && <Loading icon="dots" />} {!data && isLoading && !isFetched && <Loading icon="dots" />}
<div className={styles.footer}> <div className={styles.footer}>
{data && !error && limit && ( {showMore && data && !error && limit && (
<LinkButton href={renderUrl({ view: type })} variant="quiet"> <LinkButton href={renderUrl({ view: type })} variant="quiet">
<Text>{formatMessage(labels.more)}</Text> <Text>{formatMessage(labels.more)}</Text>
<Icon size="sm" rotate={dir === 'rtl' ? 180 : 0}> <Icon size="sm" rotate={dir === 'rtl' ? 180 : 0}>

View File

@ -4,18 +4,21 @@ import MetricsTable, { MetricsTableProps } from './MetricsTable';
import { useMessages } from 'components/hooks'; import { useMessages } from 'components/hooks';
import { useNavigation } from 'components/hooks'; import { useNavigation } from 'components/hooks';
import { emptyFilter } from 'lib/filters'; import { emptyFilter } from 'lib/filters';
import { useContext } from 'react';
import { WebsiteContext } from 'app/(main)/websites/[websiteId]/WebsiteProvider';
export interface PagesTableProps extends MetricsTableProps { export interface PagesTableProps extends MetricsTableProps {
allowFilter?: boolean; allowFilter?: boolean;
} }
export function PagesTable({ allowFilter, domainName, ...props }: PagesTableProps) { export function PagesTable({ allowFilter, ...props }: PagesTableProps) {
const { const {
router, router,
renderUrl, renderUrl,
query: { view = 'url' }, query: { view = 'url' },
} = useNavigation(); } = useNavigation();
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { domain } = useContext(WebsiteContext);
const handleSelect = (key: any) => { const handleSelect = (key: any) => {
router.push(renderUrl({ view: key }), { scroll: true }); router.push(renderUrl({ view: key }), { scroll: true });
@ -39,9 +42,7 @@ export function PagesTable({ allowFilter, domainName, ...props }: PagesTableProp
value={x} value={x}
label={!x && formatMessage(labels.none)} label={!x && formatMessage(labels.none)}
externalUrl={ externalUrl={
view === 'url' view === 'url' ? `${domain.startsWith('http') ? domain : `https://${domain}`}${x}` : null
? `${domainName.startsWith('http') ? domainName : `https://${domainName}`}${x}`
: null
} }
/> />
); );
@ -50,12 +51,11 @@ export function PagesTable({ allowFilter, domainName, ...props }: PagesTableProp
return ( return (
<MetricsTable <MetricsTable
{...props} {...props}
domainName={domainName}
title={formatMessage(labels.pages)} title={formatMessage(labels.pages)}
type={view} type={view}
metric={formatMessage(labels.views)} metric={formatMessage(labels.views)}
dataFilter={emptyFilter} dataFilter={emptyFilter}
renderLabel={renderLink} renderLabel={props.renderLabel || renderLink}
> >
{allowFilter && <FilterButtons items={buttons} selectedKey={view} onSelect={handleSelect} />} {allowFilter && <FilterButtons items={buttons} selectedKey={view} onSelect={handleSelect} />}
</MetricsTable> </MetricsTable>