Responsive updates.

This commit is contained in:
Mike Cao 2023-04-09 20:22:28 -07:00
parent 53b23420a4
commit 94ed67e339
20 changed files with 100 additions and 77 deletions

View File

@ -3,11 +3,12 @@ import { useState } from 'react';
import MobileMenu from './MobileMenu'; import MobileMenu from './MobileMenu';
import Icons from 'components/icons'; import Icons from 'components/icons';
import useMessages from 'hooks/useMessages'; import useMessages from 'hooks/useMessages';
import styles from './HamburgerButton.module.css'; import useConfig from 'hooks/useConfig';
export default function HamburgerButton() { export default function HamburgerButton() {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const [active, setActive] = useState(false); const [active, setActive] = useState(false);
const { cloudMode } = useConfig();
const menuItems = [ const menuItems = [
{ {
@ -15,7 +16,7 @@ export default function HamburgerButton() {
value: '/dashboard', value: '/dashboard',
}, },
{ label: formatMessage(labels.realtime), value: '/realtime' }, { label: formatMessage(labels.realtime), value: '/realtime' },
{ !cloudMode && {
label: formatMessage(labels.settings), label: formatMessage(labels.settings),
value: '/settings', value: '/settings',
}, },
@ -23,20 +24,15 @@ export default function HamburgerButton() {
label: formatMessage(labels.profile), label: formatMessage(labels.profile),
value: '/settings/profile', value: '/settings/profile',
}, },
{ label: formatMessage(labels.logout), value: '/logout' }, !cloudMode && { label: formatMessage(labels.logout), value: '/logout' },
]; ].filter(n => n);
function handleClick() { const handleClick = () => setActive(state => !state);
setActive(state => !state); const handleClose = () => setActive(false);
}
function handleClose() {
setActive(false);
}
return ( return (
<> <>
<Button className={styles.button} onClick={handleClick}> <Button variant="quiet" onClick={handleClick}>
<Icon>{active ? <Icons.Close /> : <Icons.Menu />}</Icon> <Icon>{active ? <Icons.Close /> : <Icons.Menu />}</Icon>
</Button> </Button>
{active && <MobileMenu items={menuItems} onClose={handleClose} />} {active && <MobileMenu items={menuItems} onClose={handleClose} />}

View File

@ -1,19 +1,10 @@
import classNames from 'classnames'; import classNames from 'classnames';
import Link from 'next/link'; import Link from 'next/link';
import { Button, Icon } from 'react-basics';
import Icons from 'components/icons';
import styles from './MobileMenu.module.css'; import styles from './MobileMenu.module.css';
export default function MobileMenu({ items = [], onClose }) { export default function MobileMenu({ items = [], onClose }) {
return ( return (
<div className={classNames(styles.menu)}> <div className={classNames(styles.menu)}>
<div className={styles.header}>
<Button onClick={onClose}>
<Icon>
<Icons.Close />
</Icon>
</Button>
</div>
<div className={styles.items}> <div className={styles.items}>
{items.map(({ label, value }) => ( {items.map(({ label, value }) => (
<Link key={value} href={value} className={styles.item} onClick={onClose}> <Link key={value} href={value} className={styles.item} onClick={onClose}>

View File

@ -1,6 +1,6 @@
.menu { .menu {
position: fixed; position: fixed;
top: 0; top: 60px;
left: 0; left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
@ -13,29 +13,17 @@
} }
.items { .items {
flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center;
align-items: center;
} }
.item { .item {
font-size: var(--font-size-lg); font-size: var(--font-size-lg);
font-weight: 700;
line-height: 80px;
padding: 0 40px;
} }
.item + .item { a.item {
margin-top: 20px; color: var(--base900);
}
.item:last-child {
margin-top: 60px;
}
.header {
display: flex;
justify-content: flex-end;
align-items: center;
height: 100px;
padding: 0 30px;
} }

View File

@ -10,6 +10,7 @@ import styles from './NavBar.module.css';
import useConfig from 'hooks/useConfig'; import useConfig from 'hooks/useConfig';
import useMessages from 'hooks/useMessages'; import useMessages from 'hooks/useMessages';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import HamburgerButton from '../common/HamburgerButton';
export default function NavBar() { export default function NavBar() {
const { pathname } = useRouter(); const { pathname } = useRouter();
@ -52,6 +53,9 @@ export default function NavBar() {
<LanguageButton /> <LanguageButton />
<ProfileButton /> <ProfileButton />
</div> </div>
<div className={styles.mobile}>
<HamburgerButton />
</div>
</Column> </Column>
</Row> </Row>
</div> </div>

View File

@ -59,7 +59,8 @@
border-bottom: 2px solid var(--primary400); border-bottom: 2px solid var(--primary400);
} }
.actions { .actions,
.mobile {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
@ -67,9 +68,17 @@
min-width: 0; min-width: 0;
} }
.mobile {
display: none;
}
@media only screen and (max-width: 768px) { @media only screen and (max-width: 768px) {
.links, .links,
.actions { .actions {
display: none; display: none;
} }
.mobile {
display: flex;
}
} }

View File

@ -6,3 +6,9 @@
position: relative; position: relative;
padding: 30px; padding: 30px;
} }
@media only screen and (max-width: 768px) {
.page {
padding: 10px 0;
}
}

View File

@ -1,12 +1,15 @@
import React from 'react'; import React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { useBreakpoint } from 'react-basics';
import styles from './PageHeader.module.css'; import styles from './PageHeader.module.css';
export default function PageHeader({ title, children, className, style }) { export default function PageHeader({ title, children }) {
const breakPoint = useBreakpoint();
return ( return (
<div className={classNames(styles.header, className)} style={style}> <div className={classNames(styles.header, { [styles[breakPoint]]: true })}>
<div className={styles.title}>{title}</div> <div className={styles.title}>{title}</div>
{children} <div className={styles.actions}>{children}</div>
</div> </div>
); );
} }

View File

@ -5,7 +5,7 @@
align-content: center; align-content: center;
align-self: stretch; align-self: stretch;
margin-bottom: 40px; margin-bottom: 40px;
height: 50px; flex-wrap: wrap;
} }
.header a { .header a {
@ -22,4 +22,10 @@
font-size: 24px; font-size: 24px;
font-weight: 700; font-weight: 700;
gap: 20px; gap: 20px;
line-height: 50px;
}
.xs .actions,
.sm .actions {
flex-basis: 100%;
} }

View File

@ -14,7 +14,7 @@ export const labels = defineMessages({
password: { id: 'label.password', defaultMessage: 'Password' }, password: { id: 'label.password', defaultMessage: 'Password' },
role: { id: 'label.role', defaultMessage: 'Role' }, role: { id: 'label.role', defaultMessage: 'Role' },
user: { id: 'label.user', defaultMessage: 'User' }, user: { id: 'label.user', defaultMessage: 'User' },
administrator: { id: 'label.administrator', defaultMessage: 'Administrator' }, admin: { id: 'label.administrator', defaultMessage: 'Administrator' },
confirm: { id: 'label.confirm', defaultMessage: 'Confirm' }, confirm: { id: 'label.confirm', defaultMessage: 'Confirm' },
details: { id: 'label.details', defaultMessage: 'Details' }, details: { id: 'label.details', defaultMessage: 'Details' },
websites: { id: 'label.websites', defaultMessage: 'Websites' }, websites: { id: 'label.websites', defaultMessage: 'Websites' },
@ -112,6 +112,7 @@ export const labels = defineMessages({
toggleCharts: { id: 'label.toggle-charts', defaultMessage: 'Toggle charts' }, toggleCharts: { id: 'label.toggle-charts', defaultMessage: 'Toggle charts' },
editDashboard: { id: 'label.edit-dashboard', defaultMessage: 'Edit dashboard' }, editDashboard: { id: 'label.edit-dashboard', defaultMessage: 'Edit dashboard' },
title: { id: 'label.title', defaultMessage: 'Title' }, title: { id: 'label.title', defaultMessage: 'Title' },
view: { id: 'label.view', defaultMessage: 'View' },
}); });
export const messages = defineMessages({ export const messages = defineMessages({

View File

@ -10,12 +10,13 @@ const MetricCard = ({
reverseColors = false, reverseColors = false,
format = formatNumber, format = formatNumber,
hideComparison = false, hideComparison = false,
className,
}) => { }) => {
const props = useSpring({ x: Number(value) || 0, from: { x: 0 } }); const props = useSpring({ x: Number(value) || 0, from: { x: 0 } });
const changeProps = useSpring({ x: Number(change) || 0, from: { x: 0 } }); const changeProps = useSpring({ x: Number(change) || 0, from: { x: 0 } });
return ( return (
<div className={styles.card}> <div className={classNames(styles.card, className)}>
<animated.div className={styles.value}>{props.x.to(x => format(x))}</animated.div> <animated.div className={styles.value}>{props.x.to(x => format(x))}</animated.div>
<div className={styles.label}> <div className={styles.label}>
{label} {label}

View File

@ -6,8 +6,8 @@ import useDateRange from 'hooks/useDateRange';
import usePageQuery from 'hooks/usePageQuery'; import usePageQuery from 'hooks/usePageQuery';
import { formatShortTime, formatNumber, formatLongNumber } from 'lib/format'; import { formatShortTime, formatNumber, formatLongNumber } from 'lib/format';
import MetricCard from './MetricCard'; import MetricCard from './MetricCard';
import styles from './MetricsBar.module.css';
import useMessages from 'hooks/useMessages'; import useMessages from 'hooks/useMessages';
import styles from './MetricsBar.module.css';
export default function MetricsBar({ websiteId }) { export default function MetricsBar({ websiteId }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
@ -58,18 +58,21 @@ export default function MetricsBar({ websiteId }) {
{data && !error && isFetched && ( {data && !error && isFetched && (
<> <>
<MetricCard <MetricCard
className={styles.card}
label={formatMessage(labels.views)} label={formatMessage(labels.views)}
value={pageviews.value} value={pageviews.value}
change={pageviews.change} change={pageviews.change}
format={formatFunc} format={formatFunc}
/> />
<MetricCard <MetricCard
className={styles.card}
label={formatMessage(labels.visitors)} label={formatMessage(labels.visitors)}
value={uniques.value} value={uniques.value}
change={uniques.change} change={uniques.change}
format={formatFunc} format={formatFunc}
/> />
<MetricCard <MetricCard
className={styles.card}
label={formatMessage(labels.bounceRate)} label={formatMessage(labels.bounceRate)}
value={uniques.value ? (num / uniques.value) * 100 : 0} value={uniques.value ? (num / uniques.value) * 100 : 0}
change={ change={
@ -82,6 +85,7 @@ export default function MetricsBar({ websiteId }) {
reverseColors reverseColors
/> />
<MetricCard <MetricCard
className={styles.card}
label={formatMessage(labels.averageVisitTime)} label={formatMessage(labels.averageVisitTime)}
value={ value={
totaltime.value && pageviews.value totaltime.value && pageviews.value

View File

@ -3,11 +3,15 @@
cursor: pointer; cursor: pointer;
min-height: 80px; min-height: 80px;
gap: 20px; gap: 20px;
flex-wrap: wrap;
}
.card {
justify-self: flex-start;
} }
@media only screen and (max-width: 992px) { @media only screen and (max-width: 992px) {
.bar { .card {
justify-content: space-between; flex-basis: calc(50% - 20px);
overflow: auto;
} }
} }

View File

@ -15,9 +15,9 @@ import useTimezone from 'hooks/useTimezone';
import usePageQuery from 'hooks/usePageQuery'; import usePageQuery from 'hooks/usePageQuery';
import { getDateArray, getDateLength } from 'lib/date'; import { getDateArray, getDateLength } from 'lib/date';
import Icons from 'components/icons'; import Icons from 'components/icons';
import styles from './WebsiteChart.module.css';
import useSticky from 'hooks/useSticky'; import useSticky from 'hooks/useSticky';
import useMessages from 'hooks/useMessages'; import useMessages from 'hooks/useMessages';
import styles from './WebsiteChart.module.css';
export default function WebsiteChart({ export default function WebsiteChart({
websiteId, websiteId,
@ -91,12 +91,14 @@ export default function WebsiteChart({
[styles.isSticky]: isSticky, [styles.isSticky]: isSticky,
})} })}
> >
<Column> <Column defaultSize={12} xl={8}>
<MetricsBar websiteId={websiteId} /> <MetricsBar websiteId={websiteId} />
</Column> </Column>
<Column className={styles.actions}> <Column defaultSize={12} xl={4}>
<RefreshButton websiteId={websiteId} isLoading={isLoading} /> <div className={styles.actions}>
<DateFilter websiteId={websiteId} value={value} className={styles.dropdown} /> <RefreshButton websiteId={websiteId} isLoading={isLoading} />
<DateFilter websiteId={websiteId} value={value} className={styles.dropdown} />
</div>
</Column> </Column>
</Row> </Row>
<Row> <Row>

View File

@ -27,23 +27,31 @@
z-index: var(--z-index-above); z-index: var(--z-index-above);
} }
.sticky {
position: sticky;
top: -1px;
}
.isSticky {
border-bottom: 1px solid var(--base300);
}
.actions { .actions {
display: flex; display: flex;
align-items: center;
flex-direction: row; flex-direction: row;
justify-content: flex-end; justify-content: flex-end;
gap: 10px; gap: 10px;
flex: 1;
} }
.dropdown { .dropdown {
min-width: 200px; min-width: 200px;
} }
@media only screen and (min-width: 992px) {
.sticky {
position: sticky;
top: -1px;
}
.isSticky {
border-bottom: 1px solid var(--base300);
}
}
@media only screen and (max-width: 1200px) {
.actions {
margin-top: 40px;
}
}

View File

@ -6,11 +6,11 @@ import styles from './WebsiteHeader.module.css';
export default function WebsiteHeader({ websiteId, name, domain, children }) { export default function WebsiteHeader({ websiteId, name, domain, children }) {
return ( return (
<Row className={styles.header} justifyContent="center"> <Row className={styles.header} justifyContent="center">
<Column className={styles.name} variant="two"> <Column className={styles.title} variant="two">
<Favicon domain={domain} /> <Favicon domain={domain} />
<Text>{name}</Text> <Text>{name}</Text>
</Column> </Column>
<Column className={styles.body} variant="two"> <Column className={styles.info} variant="two">
<ActiveUsers websiteId={websiteId} /> <ActiveUsers websiteId={websiteId} />
{children} {children}
</Column> </Column>

View File

@ -1,8 +1,4 @@
.header { .title {
height: 100px;
}
.name {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
@ -10,12 +6,14 @@
font-size: 24px; font-size: 24px;
font-weight: 700; font-weight: 700;
overflow: hidden; overflow: hidden;
height: 100px;
} }
.body { .info {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
justify-content: flex-end; justify-content: flex-end;
gap: 30px; gap: 30px;
min-height: 0;
} }

View File

@ -30,7 +30,7 @@ export default function Dashboard({ userId }) {
{!editing && hasData && <DashboardSettingsButton />} {!editing && hasData && <DashboardSettingsButton />}
</PageHeader> </PageHeader>
{!hasData && ( {!hasData && (
<EmptyPlaceholder message={formatMessage(messages.noWebsites)}> <EmptyPlaceholder message={formatMessage(messages.noWebsitesConfigured)}>
<Link href="/settings/websites"> <Link href="/settings/websites">
<Button> <Button>
<Icon> <Icon>

View File

@ -50,7 +50,7 @@ export default function UsersTable({ data = [], onDelete }) {
addSuffix: true, addSuffix: true,
}), }),
role: formatMessage( role: formatMessage(
labels[Object.keys(ROLES).find(key => ROLES[key] === row.role) || labels.unknown], labels[Object.keys(ROLES).find(key => ROLES[key] === row.role)] || labels.unknown,
), ),
action: ( action: (
<> <>

View File

@ -45,7 +45,7 @@ export default function WebsitesList() {
<PageHeader title={formatMessage(labels.websites)}>{addButton}</PageHeader> <PageHeader title={formatMessage(labels.websites)}>{addButton}</PageHeader>
{hasData && <WebsitesTable data={data} />} {hasData && <WebsitesTable data={data} />}
{!hasData && ( {!hasData && (
<EmptyPlaceholder message={formatMessage(messages.noWebsites)}> <EmptyPlaceholder message={formatMessage(messages.noWebsitesConfigured)}>
{addButton} {addButton}
</EmptyPlaceholder> </EmptyPlaceholder>
)} )}

View File

@ -11,6 +11,7 @@ import {
Icon, Icon,
Icons, Icons,
Flexbox, Flexbox,
useBreakpoint,
} from 'react-basics'; } from 'react-basics';
import useMessages from 'hooks/useMessages'; import useMessages from 'hooks/useMessages';
import useConfig from 'hooks/useConfig'; import useConfig from 'hooks/useConfig';
@ -18,11 +19,12 @@ import useConfig from 'hooks/useConfig';
export default function WebsitesTable({ data = [] }) { export default function WebsitesTable({ data = [] }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { openExternal } = useConfig(); const { openExternal } = useConfig();
const breakPoint = useBreakpoint();
const columns = [ const columns = [
{ name: 'name', label: formatMessage(labels.name), style: { flex: 2 } }, { name: 'name', label: formatMessage(labels.name), style: { flex: 2 } },
{ name: 'domain', label: formatMessage(labels.domain) }, { name: 'domain', label: formatMessage(labels.domain) },
{ name: 'action', label: ' ' }, { name: 'action', label: ' ', style: { flexBasis: '100%' } },
]; ];
return ( return (