CSS updates.

This commit is contained in:
Mike Cao 2023-10-02 23:51:26 -07:00
parent 9bb89c7e8b
commit e4c5f42189
22 changed files with 227 additions and 395 deletions

View File

@ -9,7 +9,7 @@ import Head from 'next/head';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import Script from 'next/script';
import { Button, Column, Row } from 'react-basics';
import { Button } from 'react-basics';
import styles from './TestConsole.module.css';
export function TestConsole() {
@ -91,8 +91,8 @@ export function TestConsole() {
src={`${basePath}/script.js`}
data-cache="true"
/>
<Row className={styles.test}>
<Column xs="4">
<div className={styles.test}>
<div>
<div className={styles.header}>Page links</div>
<div>
<Link href={`/console/${websiteId}/page/1/?q=abc`}>page one</Link>
@ -115,8 +115,8 @@ export function TestConsole() {
external link (tab)
</a>
</div>
</Column>
<Column xs="4">
</div>
<div>
<div className={styles.header}>Click events</div>
<Button id="send-event-button" data-umami-event="button-click" variant="action">
Send event
@ -131,8 +131,8 @@ export function TestConsole() {
>
Send event with data
</Button>
</Column>
<Column xs="4">
</div>
<div>
<div className={styles.header}>Javascript events</div>
<Button id="manual-button" variant="action" onClick={handleClick}>
Run script
@ -141,14 +141,12 @@ export function TestConsole() {
<Button id="manual-button" variant="action" onClick={handleIdentifyClick}>
Run identify
</Button>
</Column>
</Row>
<Row>
<Column>
</div>
</div>
<div>
<WebsiteChart websiteId={website.id} />
<EventsChart websiteId={website.id} />
</Column>
</Row>
</div>
</>
)}
</Page>

View File

@ -1,5 +1,5 @@
import { useState } from 'react';
import { GridRow, GridColumn } from 'components/layout/Grid';
import { Grid, GridRow } from 'components/layout/Grid';
import PagesTable from 'components/metrics/PagesTable';
import ReferrersTable from 'components/metrics/ReferrersTable';
import BrowsersTable from 'components/metrics/BrowsersTable';
@ -18,42 +18,24 @@ export default function WebsiteTableView({ websiteId }) {
};
return (
<>
<GridRow>
<GridColumn variant="two">
<Grid>
<GridRow columns="two">
<PagesTable {...tableProps} />
</GridColumn>
<GridColumn variant="two">
<ReferrersTable {...tableProps} />
</GridColumn>
</GridRow>
<GridRow>
<GridColumn variant="three">
<GridRow columns="three">
<BrowsersTable {...tableProps} />
</GridColumn>
<GridColumn variant="three">
<OSTable {...tableProps} />
</GridColumn>
<GridColumn variant="three">
<DevicesTable {...tableProps} />
</GridColumn>
</GridRow>
<GridRow>
<GridColumn xs={12} sm={12} md={12} defaultSize={8}>
<GridRow columns="two-one">
<WorldMap data={countryData} />
</GridColumn>
<GridColumn xs={12} sm={12} md={12} defaultSize={4}>
<CountriesTable {...tableProps} onDataLoad={setCountryData} />
</GridColumn>
</GridRow>
<GridRow>
<GridColumn xs={12} sm={12} md={12} lg={4} defaultSize={4}>
<GridRow columns="one-two">
<EventsTable {...tableProps} />
</GridColumn>
<GridColumn xs={12} sm={12} md={12} lg={8} defaultSize={8}>
<EventsChart websiteId={websiteId} />
</GridColumn>
</GridRow>
</>
</Grid>
);
}

View File

@ -1,35 +0,0 @@
.col {
display: flex;
flex-direction: column;
}
.row {
border-top: 1px solid var(--base300);
min-height: 430px;
}
.row > .col {
border-left: 1px solid var(--base300);
padding: 20px;
}
.row > .col:first-child {
border-left: 0;
padding-left: 0;
}
.row > .col:last-child {
padding-right: 0;
}
@media only screen and (max-width: 992px) {
.row {
border: 0;
}
.row > .col {
border-top: 1px solid var(--base300);
border-left: 0;
padding: 20px 0;
}
}

View File

@ -1,6 +1,6 @@
'use client';
import classNames from 'classnames';
import { Row, Column, Text, Button, Icon } from 'react-basics';
import { Text, Button, Icon } from 'react-basics';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import Favicon from 'components/common/Favicon';
@ -39,17 +39,19 @@ export function WebsiteHeader({ websiteId, showLinks = true, children }) {
];
return (
<Row className={styles.header} justifyContent="center">
<Column className={styles.title} variant="two">
<div className={styles.header}>
<div className={styles.title}>
<Favicon domain={domain} />
<Text>{name}</Text>
<ActiveUsers websiteId={websiteId} />
</Column>
<Column className={styles.actions} variant="two">
</div>
<div className={styles.actions}>
{showLinks && (
<div className={styles.links}>
{links.map(({ label, icon, path }) => {
const selected = path ? pathname.endsWith(path) : pathname === '/websites/[id]';
const selected = path
? pathname.endsWith(path)
: pathname.match(/^\/websites\/[\w-]+$/);
return (
<Link key={label} href={`/websites/${websiteId}${path}`} shallow={true}>
@ -68,8 +70,8 @@ export function WebsiteHeader({ websiteId, showLinks = true, children }) {
</div>
)}
{children}
</Column>
</Row>
</div>
</div>
);
}

View File

@ -1,6 +1,6 @@
.header {
display: flex;
flex-direction: row;
display: grid;
grid-template-columns: 1fr max-content;
align-items: center;
}
@ -35,6 +35,10 @@
}
@media only screen and (max-width: 768px) {
.header {
grid-template-columns: 1fr;
}
.links {
justify-content: space-evenly;
flex: 1;
@ -49,7 +53,7 @@
.icon,
.icon svg {
width: 30px;
height: 30px;
width: 20px;
height: 20px;
}
}

View File

@ -6,7 +6,7 @@ import MetricsBar from 'components/metrics/MetricsBar';
import FilterSelectForm from '../../reports/FilterSelectForm';
import PopupForm from '../../reports/PopupForm';
import { formatShortTime } from 'lib/format';
import { Button, Column, Icon, Icons, Popup, PopupTrigger, Row } from 'react-basics';
import { Button, Icon, Icons, Popup, PopupTrigger } from 'react-basics';
import styles from './WebsiteMetricsBar.module.css';
export function WebsiteMetricsBar({ websiteId, showFilter = true, sticky }) {
@ -98,25 +98,22 @@ export function WebsiteMetricsBar({ websiteId, showFilter = true, sticky }) {
};
return (
<Row
<div
ref={ref}
className={classNames(styles.container, {
[styles.sticky]: sticky,
[styles.isSticky]: isSticky,
})}
>
<Column defaultSize={12} xl={8}>
<MetricsBar isLoading={isLoading} isFetched={isFetched} error={error}>
{!error && isFetched && (
{pageviews && uniques && (
<>
<MetricCard
className={styles.card}
label={formatMessage(labels.views)}
value={pageviews.value}
change={pageviews.change}
/>
<MetricCard
className={styles.card}
label={formatMessage(labels.visitors)}
value={uniques.value}
change={uniques.change}
@ -149,21 +146,16 @@ export function WebsiteMetricsBar({ websiteId, showFilter = true, sticky }) {
-1 || 0
: 0
}
format={n =>
`${n < 0 ? '-' : ''}${formatShortTime(Math.abs(~~n), ['m', 's'], ' ')}`
}
format={n => `${n < 0 ? '-' : ''}${formatShortTime(Math.abs(~~n), ['m', 's'], ' ')}`}
/>
</>
)}
</MetricsBar>
</Column>
<Column defaultSize={12} xl={4}>
<div className={styles.actions}>
{showFilter && <WebsiteFilterButton />}
<WebsiteDateFilter websiteId={websiteId} />
</div>
</Column>
</Row>
</div>
);
}

View File

@ -1,12 +1,12 @@
.container {
display: flex;
display: grid;
grid-template-columns: 1fr max-content;
justify-content: space-between;
align-items: center;
padding: 10px 0;
min-height: 90px;
margin-bottom: 20px;
background: var(--base50);
z-index: var(--z-index-above);
min-height: 120px;
padding-bottom: 20px;
}
.actions {
@ -18,8 +18,12 @@
}
@media only screen and (max-width: 1200px) {
.container {
grid-template-columns: 1fr;
}
.actions {
margin-top: 40px;
margin: 20px 0;
}
}
@ -30,6 +34,7 @@
}
.isSticky {
padding: 10px 0;
border-bottom: 1px solid var(--base300);
}
}

View File

@ -1,4 +1,3 @@
import { Column, Row } from 'react-basics';
import { useApi, useDateRange } from 'components/hooks';
import MetricCard from 'components/metrics/MetricCard';
import useMessages from 'components/hooks/useMessages';
@ -23,36 +22,16 @@ export function EventDataMetricsBar({ websiteId }) {
);
return (
<Row className={styles.row}>
<Column defaultSize={12} xl={8}>
<div className={styles.container}>
<MetricsBar isLoading={isLoading} isFetched={isFetched} error={error}>
{!error && isFetched && (
<>
<MetricCard
className={styles.card}
label={formatMessage(labels.events)}
value={data?.events}
/>
<MetricCard
className={styles.card}
label={formatMessage(labels.fields)}
value={data?.fields}
/>
<MetricCard
className={styles.card}
label={formatMessage(labels.totalRecords)}
value={data?.records}
/>
</>
)}
<MetricCard label={formatMessage(labels.events)} value={data?.events} />
<MetricCard label={formatMessage(labels.fields)} value={data?.fields} />
<MetricCard label={formatMessage(labels.totalRecords)} value={data?.records} />
</MetricsBar>
</Column>
<Column defaultSize={12} xl={4}>
<div className={styles.actions}>
<WebsiteDateFilter websiteId={websiteId} />
</div>
</Column>
</Row>
</div>
);
}

View File

@ -1,5 +1,6 @@
.container {
display: flex;
display: grid;
grid-template-columns: 1fr 1fr;
justify-content: space-between;
align-items: center;
padding: 10px 0;
@ -9,12 +10,6 @@
z-index: var(--z-index-above);
}
.metrics {
display: flex;
flex-direction: row;
align-items: center;
}
.actions {
display: flex;
flex-direction: row;
@ -23,24 +18,9 @@
flex: 1;
}
.bar {
display: flex;
cursor: pointer;
min-height: 110px;
gap: 20px;
flex-wrap: wrap;
}
.card {
justify-self: flex-start;
}
@media only screen and (max-width: 992px) {
.card {
flex-basis: calc(50% - 20px);
.container {
grid-template-columns: 1fr;
grid-template-rows: 1fr 1fr;
}
}
.row {
border-bottom: 1px solid var(--border-color);
}

View File

@ -2,7 +2,7 @@
import { useMemo, useState, useEffect } from 'react';
import { subMinutes, startOfMinute } from 'date-fns';
import firstBy from 'thenby';
import { GridRow, GridColumn } from 'components/layout/Grid';
import { Grid, GridRow } from 'components/layout/Grid';
import Page from 'components/layout/Page';
import RealtimeChart from 'components/metrics/RealtimeChart';
import WorldMap from 'components/common/WorldMap';
@ -99,22 +99,16 @@ export function Realtime({ websiteId }) {
<WebsiteHeader websiteId={websiteId} />
<RealtimeHeader websiteId={websiteId} data={currentData} />
<RealtimeChart className={styles.chart} data={realtimeData} unit="minute" />
<GridRow>
<GridColumn xs={12} sm={12} md={12} lg={4} xl={4}>
<Grid>
<GridRow columns="one-two">
<RealtimeUrls websiteId={websiteId} websiteDomain={website?.domain} data={realtimeData} />
</GridColumn>
<GridColumn xs={12} sm={12} md={12} lg={8} xl={8}>
<RealtimeLog websiteId={websiteId} websiteDomain={website?.domain} data={realtimeData} />
</GridColumn>
</GridRow>
<GridRow>
<GridColumn xs={12} lg={4}>
<GridRow columns="one-two">
<RealtimeCountries data={realtimeData?.countries} />
</GridColumn>
<GridColumn xs={12} lg={8}>
<WorldMap data={realtimeData?.countries} />
</GridColumn>
</GridRow>
</Grid>
</>
);
}

View File

@ -1,117 +0,0 @@
import { useState, useEffect, useMemo } from 'react';
import { subMinutes, startOfMinute } from 'date-fns';
import firstBy from 'thenby';
import { GridRow, GridColumn } from 'components/layout/Grid';
import Page from 'components/layout/Page';
import RealtimeChart from 'components/metrics/RealtimeChart';
import WorldMap from 'components/common/WorldMap';
import RealtimeLog from './RealtimeLog';
import RealtimeHeader from './RealtimeHeader';
import RealtimeUrls from './RealtimeUrls';
import RealtimeCountries from './RealtimeCountries';
import WebsiteHeader from '../WebsiteHeader';
import useApi from 'components/hooks/useApi';
import { percentFilter } from 'lib/filters';
import { REALTIME_RANGE, REALTIME_INTERVAL } from 'lib/constants';
import { useWebsite } from 'components/hooks';
import styles from './Realtime.module.css';
function mergeData(state = [], data = [], time) {
const ids = state.map(({ __id }) => __id);
return state
.concat(data.filter(({ __id }) => !ids.includes(__id)))
.filter(({ timestamp }) => timestamp >= time);
}
export function RealtimePage({ websiteId }) {
const [currentData, setCurrentData] = useState();
const { get, useQuery } = useApi();
const { data: website } = useWebsite(websiteId);
const { data, isLoading, error } = useQuery(
['realtime', websiteId],
() => get(`/realtime/${websiteId}`, { startAt: currentData?.timestamp || 0 }),
{
enabled: !!(websiteId && website),
refetchInterval: REALTIME_INTERVAL,
cache: false,
},
);
useEffect(() => {
if (data) {
const date = subMinutes(startOfMinute(new Date()), REALTIME_RANGE);
const time = date.getTime();
setCurrentData(state => ({
pageviews: mergeData(state?.pageviews, data.pageviews, time),
sessions: mergeData(state?.sessions, data.sessions, time),
events: mergeData(state?.events, data.events, time),
timestamp: data.timestamp,
}));
}
}, [data]);
const realtimeData = useMemo(() => {
if (!currentData) {
return { pageviews: [], sessions: [], events: [], countries: [], visitors: [] };
}
currentData.countries = percentFilter(
currentData.sessions
.reduce((arr, data) => {
if (!arr.find(({ id }) => id === data.id)) {
return arr.concat(data);
}
return arr;
}, [])
.reduce((arr, { country }) => {
if (country) {
const row = arr.find(({ x }) => x === country);
if (!row) {
arr.push({ x: country, y: 1 });
} else {
row.y += 1;
}
}
return arr;
}, [])
.sort(firstBy('y', -1)),
);
currentData.visitors = currentData.sessions.reduce((arr, val) => {
if (!arr.find(({ id }) => id === val.id)) {
return arr.concat(val);
}
return arr;
}, []);
return currentData;
}, [currentData]);
return (
<Page loading={isLoading} error={error}>
<WebsiteHeader websiteId={websiteId} />
<RealtimeHeader websiteId={websiteId} data={currentData} />
<RealtimeChart className={styles.chart} data={realtimeData} unit="minute" />
<GridRow>
<GridColumn xs={12} sm={12} md={12} lg={4} xl={4}>
<RealtimeUrls websiteId={websiteId} websiteDomain={website?.domain} data={realtimeData} />
</GridColumn>
<GridColumn xs={12} sm={12} md={12} lg={8} xl={8}>
<RealtimeLog websiteId={websiteId} websiteDomain={website?.domain} data={realtimeData} />
</GridColumn>
</GridRow>
<GridRow>
<GridColumn xs={12} lg={4}>
<RealtimeCountries data={realtimeData?.countries} />
</GridColumn>
<GridColumn xs={12} lg={8}>
<WorldMap data={realtimeData?.countries} />
</GridColumn>
</GridRow>
</Page>
);
}
export default RealtimePage;

View File

@ -1,7 +1,7 @@
'use client';
import { useEffect, useCallback, useState } from 'react';
import { createPortal } from 'react-dom';
import { Button, Row, Column } from 'react-basics';
import { Button } from 'react-basics';
import { setItem } from 'next-basics';
import useStore, { checkVersion } from 'store/version';
import { REPO_URL, VERSION_CHECK } from 'lib/constants';
@ -47,17 +47,17 @@ export function UpdateNotice({ user, config }) {
}
return createPortal(
<Row className={styles.notice}>
<Column variant="two" className={styles.message}>
<div className={styles.notice}>
<div className={styles.message}>
{formatMessage(messages.newVersionAvailable, { version: `v${latest}` })}
</Column>
<Column className={styles.buttons}>
</div>
<div className={styles.buttons}>
<Button variant="primary" onClick={handleViewClick}>
{formatMessage(labels.viewDetails)}
</Button>
<Button onClick={handleDismissClick}>{formatMessage(labels.dismiss)}</Button>
</Column>
</Row>,
</div>
</div>,
document.body,
);
}

View File

@ -1,7 +1,7 @@
import useDateRange from 'components/hooks/useDateRange';
import { isAfter } from 'date-fns';
import { incrementDateRange } from 'lib/date';
import { Button, Flexbox, Icon, Icons } from 'react-basics';
import { Button, Icon, Icons } from 'react-basics';
import DateFilter from './DateFilter';
import styles from './WebsiteDateFilter.module.css';
@ -22,9 +22,9 @@ export function WebsiteDateFilter({ websiteId }) {
};
return (
<Flexbox justifyContent="center" gap={10}>
<div className={styles.container}>
{value !== 'all' && selectedUnit && (
<Flexbox justifyContent="center" className={styles.buttons}>
<div className={styles.buttons}>
<Button onClick={() => handleIncrement(1)}>
<Icon rotate={90}>
<Icons.ChevronDown />
@ -35,7 +35,7 @@ export function WebsiteDateFilter({ websiteId }) {
<Icons.ChevronDown />
</Icon>
</Button>
</Flexbox>
</div>
)}
<DateFilter
className={styles.dropdown}
@ -46,7 +46,7 @@ export function WebsiteDateFilter({ websiteId }) {
onChange={handleChange}
showAllTime={true}
/>
</Flexbox>
</div>
);
}

View File

@ -1,7 +1,17 @@
.container {
display: flex;
align-items: center;
gap: 10px;
}
.dropdown {
min-width: 200px;
}
.buttons {
display: flex;
}
.buttons button:first-child {
border-top-right-radius: 0;
border-bottom-right-radius: 0;

View File

@ -1,13 +1,18 @@
import { Row, Column } from 'react-basics';
import classNames from 'classnames';
import styles from './Grid.module.css';
import { mapChildren } from 'react-basics';
export function Grid({ className, ...otherProps }) {
return <div {...otherProps} className={classNames(styles.grid, className)} />;
}
export function GridRow(props) {
const { className, ...otherProps } = props;
return <Row {...otherProps} className={classNames(styles.row, className)} />;
}
export function GridColumn(props) {
const { className, ...otherProps } = props;
return <Column {...otherProps} className={classNames(styles.col, className)} />;
const { columns = 'two', className, children, ...otherProps } = props;
return (
<div {...otherProps} className={classNames(styles.row, className)}>
{mapChildren(children, child => {
return <div className={classNames(styles.col, { [styles[columns]]: true })}>{child}</div>;
})}
</div>
);
}

View File

@ -1,27 +1,52 @@
.col {
display: flex;
flex-direction: column;
padding: 20px;
.grid {
display: grid;
}
.row {
display: grid;
grid-template-columns: repeat(6, 1fr);
border-top: 1px solid var(--base300);
min-height: 430px;
}
.row > .col {
.col {
padding: 20px;
min-height: 430px;
border-inline-start: 1px solid var(--base300);
}
.row > .col:first-child {
.col:first-child {
border-inline-start: 0;
padding-inline-start: 0;
}
.row > .col:last-child {
.col:last-child {
padding-inline-end: 0;
}
.col.two {
grid-column: span 3;
}
.col.three {
grid-column: span 2;
}
.col.two-one:first-child {
grid-column: span 4;
}
.col.two-one:last-child {
grid-column: span 2;
}
.col.one-two:first-child {
grid-column: span 2;
}
.col.one-two:last-child {
grid-column: span 4;
}
@media only screen and (max-width: 992px) {
.row {
border: 0;
@ -33,4 +58,11 @@
border-inline-end: 0;
padding: 20px 0;
}
.col.two,
.col.three,
.col.one-two,
.col.two-one {
grid-column: span 6 !important;
}
}

View File

@ -4,7 +4,6 @@
flex-direction: column;
background: var(--base50);
position: relative;
height: 100%;
max-width: 1320px;
margin: 0 auto;
padding: 0 20px;

View File

@ -145,7 +145,7 @@ export function BarChart({
}, [datasets, unit, theme, animationDuration, locale]);
return (
<>
<div className={styles.container}>
<div className={classNames(styles.chart, className)}>
{loading && <Loading position="page" icon="dots" />}
<canvas ref={canvas} />
@ -156,7 +156,7 @@ export function BarChart({
<div className={styles.tooltip}>{tooltip}</div>
</HoverTooltip>
)}
</>
</div>
);
}

View File

@ -1,3 +1,7 @@
.container {
display: grid;
}
.chart {
position: relative;
height: 400px;

View File

@ -1,8 +1,8 @@
import { useState } from 'react';
import { Loading, cloneChildren } from 'react-basics';
import ErrorMessage from 'components/common/ErrorMessage';
import styles from './MetricsBar.module.css';
import { formatLongNumber, formatNumber } from 'lib/format';
import styles from './MetricsBar.module.css';
export function MetricsBar({ children, isLoading, isFetched, error }) {
const [format, setFormat] = useState(true);
@ -19,7 +19,10 @@ export function MetricsBar({ children, isLoading, isFetched, error }) {
<div className={styles.bar} onClick={handleSetFormat}>
{isLoading && !isFetched && <Loading icon="dots" />}
{error && <ErrorMessage />}
{cloneChildren(children, child => {
{!isLoading &&
!error &&
isFetched &&
cloneChildren(children, child => {
return { format: child.props.format || formatFunc };
})}
</div>

View File

@ -1,18 +1,11 @@
.bar {
display: flex;
flex-direction: row;
cursor: pointer;
min-height: 110px;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, max-content));
gap: 20px;
flex-wrap: wrap;
}
.card {
justify-self: flex-start;
}
@media only screen and (max-width: 992px) {
.card {
flex-basis: calc(50% - 20px);
@media screen and (max-width: 768px) {
.bar {
grid-template-columns: 1fr 1fr;
}
}

View File

@ -52,6 +52,8 @@ main {
svg {
shape-rendering: geometricPrecision;
width: 10px;
height: 10px;
}
#__next {