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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,7 +5,7 @@
align-content: center;
align-self: stretch;
margin-bottom: 40px;
height: 50px;
flex-wrap: wrap;
}
.header a {
@ -22,4 +22,10 @@
font-size: 24px;
font-weight: 700;
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' },
role: { id: 'label.role', defaultMessage: 'Role' },
user: { id: 'label.user', defaultMessage: 'User' },
administrator: { id: 'label.administrator', defaultMessage: 'Administrator' },
admin: { id: 'label.administrator', defaultMessage: 'Administrator' },
confirm: { id: 'label.confirm', defaultMessage: 'Confirm' },
details: { id: 'label.details', defaultMessage: 'Details' },
websites: { id: 'label.websites', defaultMessage: 'Websites' },
@ -112,6 +112,7 @@ export const labels = defineMessages({
toggleCharts: { id: 'label.toggle-charts', defaultMessage: 'Toggle charts' },
editDashboard: { id: 'label.edit-dashboard', defaultMessage: 'Edit dashboard' },
title: { id: 'label.title', defaultMessage: 'Title' },
view: { id: 'label.view', defaultMessage: 'View' },
});
export const messages = defineMessages({

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -50,7 +50,7 @@ export default function UsersTable({ data = [], onDelete }) {
addSuffix: true,
}),
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: (
<>

View File

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

View File

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