Feature/details modal mobile (#1211)

* chore(docker): improve repeat contributions workflow

* This change adds two new commands to gracefully stop and remove the Postgres and Clickhouse docker containers. To do so, it also gives them a recognizable name.

* Additionally, the Postgres container is updated to use a named volume for its data. This lower friction for repeat contributions where one would otherwise sign up and activate their accounts again and again each time.

* Format countries modal

* Remove unused imports
* Run ESLint and make related fixes

* ESlint formatting for entry pages modal

* WIP: proof of concept for scrollable modals on mobile

* Fix modals being too wide on desktop

* Make modals truly responsive

This fixes the desktop behaviour completely now.

* Update changelog with modals responsiveness

* Update desktop modal width to 860px

It was an oversight to set it at 800px in the first place.

Co-authored-by: Uku Taht <Uku.taht@gmail.com>
This commit is contained in:
Ru Singh 2021-08-13 13:30:42 +05:30 committed by GitHub
parent 99e24765d8
commit dd20fc8a17
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 132 additions and 41 deletions

View File

@ -26,6 +26,7 @@ All notable changes to this project will be documented in this file.
- UI fix for the main graph on mobile overlapping its tick items on both axis
- UI fixes for text not showing properly in bars across multiple lines. This hides the totals on <768px and only shows the uniques and % to accommodate the goals text too. Larger screens still truncate as usual.
- Turn off autocomplete for name and password inputs in the _New shared link_ form.
- Details modals are now responsive and take up less horizontal space on smaller screens to make it easier to scroll.
### Removed
- Removes AppSignal monitoring package

View File

@ -31,7 +31,7 @@
bottom: 0;
background: rgba(0,0,0,0.6);
z-index: 99;
overflow-x: hidden;
overflow-x: auto;
overflow-y: auto;
}

View File

@ -5,7 +5,6 @@ import Datamap from 'datamaps'
import Modal from './modal'
import * as api from '../../api'
import numberFormatter from '../../number-formatter'
import Bar from '../bar'
import {parseQuery} from '../../query'
class CountriesModal extends React.Component {
@ -22,6 +21,10 @@ class CountriesModal extends React.Component {
.then((res) => this.setState({loading: false, countries: res}))
}
label() {
return this.state.query.period === 'realtime' ? 'Current visitors' : 'Visitors'
}
renderCountry(country) {
const query = new URLSearchParams(window.location.search)
query.set('country', country.name)
@ -33,7 +36,11 @@ class CountriesModal extends React.Component {
return (
<tr className="text-sm dark:text-gray-200" key={country.name}>
<td className="p-2">
<Link className="hover:underline" to={{search: query.toString(), pathname: '/' + encodeURIComponent(this.props.site.domain)}}>
<Link
className="hover:underline"
to={{search: query.toString(),
pathname: `/${ encodeURIComponent(this.props.site.domain)}`}}
>
{countryFullName}
</Link>
</td>
@ -44,27 +51,36 @@ class CountriesModal extends React.Component {
)
}
label() {
return this.state.query.period === 'realtime' ? 'Current visitors' : 'Visitors'
}
renderBody() {
if (this.state.loading) {
return (
<div className="loading mt-32 mx-auto"><div></div></div>
)
} else if (this.state.countries) {
}
if (this.state.countries) {
return (
<React.Fragment>
<>
<h1 className="text-xl font-bold dark:text-gray-100">Top countries</h1>
<div className="my-4 border-b border-gray-300 dark:border-gray-500"></div>
<main className="modal__content">
<table className="w-full table-striped table-fixed">
<table className="w-max overflow-x-auto md:w-full table-striped table-fixed">
<thead>
<tr>
<th className="p-2 text-xs tracking-wide font-bold text-gray-500 dark:text-gray-400" align="left">Country</th>
<th className="p-2 w-32 text-xs tracking-wide font-bold text-gray-500 dark:text-gray-400" align="right">{this.label()}</th>
<th
className="p-2 w-48 lg:w-1/2 text-xs tracking-wide font-bold text-gray-500 dark:text-gray-400"
align="left"
>
Country
</th>
<th
// eslint-disable-next-line max-len
className="p-2 w-32 lg:w-1/2 text-xs tracking-wide font-bold text-gray-500 dark:text-gray-400"
align="right"
>
{this.label()}
</th>
</tr>
</thead>
<tbody>
@ -72,7 +88,7 @@ class CountriesModal extends React.Component {
</tbody>
</table>
</main>
</React.Fragment>
</>
)
}
}

View File

@ -1,6 +1,6 @@
import React from "react";
import { Link } from 'react-router-dom'
import { withRouter } from 'react-router-dom'
import { Link , withRouter } from 'react-router-dom'
import Modal from './modal'
import * as api from '../../api'
@ -26,12 +26,23 @@ class EntryPagesModal extends React.Component {
loadPages() {
const {query, page, pages} = this.state;
api.get(`/api/stats/${encodeURIComponent(this.props.site.domain)}/entry-pages`, query, {limit: 100, page})
.then((res) => this.setState((state) => ({loading: false, pages: state.pages.concat(res), moreResultsAvailable: res.length === 100})))
api.get(
`/api/stats/${encodeURIComponent(this.props.site.domain)}/entry-pages`,
query,
{limit: 100, page}
)
.then(
(res) => this.setState((state) => ({
loading: false,
pages: state.pages.concat(res),
moreResultsAvailable: res.length === 100
}))
)
}
loadMore() {
this.setState({loading: true, page: this.state.page + 1}, this.loadPages.bind(this))
const { page } = this.state;
this.setState({loading: true, page: page + 1}, this.loadPages.bind(this))
}
showVisitDuration() {
@ -40,10 +51,9 @@ class EntryPagesModal extends React.Component {
formatBounceRate(page) {
if (typeof(page.bounce_rate) === 'number') {
return page.bounce_rate + '%'
} else {
return '-'
return `${page.bounce_rate}%`;
}
return '-';
}
renderPage(page) {
@ -53,7 +63,15 @@ class EntryPagesModal extends React.Component {
return (
<tr className="text-sm dark:text-gray-200" key={page.name}>
<td className="p-2">
<Link to={{pathname: `/${encodeURIComponent(this.props.site.domain)}`, search: query.toString()}} className="hover:underline">{page.name}</Link>
<Link
to={{
pathname: `/${encodeURIComponent(this.props.site.domain)}`,
search: query.toString()
}}
className="hover:underline"
>
{page.name}
</Link>
</td>
<td className="p-2 w-32 font-medium" align="right">{numberFormatter(page.count)}</td>
<td className="p-2 w-32 font-medium" align="right">{numberFormatter(page.entries)}</td>
@ -79,18 +97,34 @@ class EntryPagesModal extends React.Component {
renderBody() {
if (this.state.pages) {
return (
<React.Fragment>
<>
<h1 className="text-xl font-bold dark:text-gray-100">Entry Pages</h1>
<div className="my-4 border-b border-gray-300"></div>
<main className="modal__content">
<table className="w-full table-striped table-fixed">
<table className="w-max overflow-x-auto md:w-full table-striped table-fixed">
<thead>
<tr>
<th className="p-2 text-xs tracking-wide font-bold text-gray-500 dark:text-gray-400" align="left">Page url</th>
<th className="p-2 w-32 text-xs tracking-wide font-bold text-gray-500 dark:text-gray-400" align="right">Unique Entrances</th>
<th className="p-2 w-32 text-xs tracking-wide font-bold text-gray-500 dark:text-gray-400" align="right">Total Entrances</th>
{<th className="p-2 w-32 text-xs tracking-wide font-bold text-gray-500 dark:text-gray-400" align="right">Visit Duration</th>}
<th
className="p-2 w-48 md:w-56 lg:w-1/3 text-xs tracking-wide font-bold text-gray-500 dark:text-gray-400"
align="left"
>Page url
</th>
<th
className="p-2 w-32 text-xs tracking-wide font-bold text-gray-500 dark:text-gray-400"
align="right"
>Unique Entrances
</th>
<th
className="p-2 w-32 text-xs tracking-wide font-bold text-gray-500 dark:text-gray-400"
align="right"
>Total Entrances
</th>
<th
className="p-2 w-32 text-xs tracking-wide font-bold text-gray-500 dark:text-gray-400"
align="right"
>Visit Duration
</th>
</tr>
</thead>
<tbody>
@ -98,7 +132,7 @@ class EntryPagesModal extends React.Component {
</tbody>
</table>
</main>
</React.Fragment>
</>
)
}
}

View File

@ -80,10 +80,10 @@ class ExitPagesModal extends React.Component {
<div className="my-4 border-b border-gray-300"></div>
<main className="modal__content">
<table className="w-full table-striped table-fixed">
<table className="w-max overflow-x-auto md:w-full table-striped table-fixed">
<thead>
<tr>
<th className="p-2 text-xs tracking-wide font-bold text-gray-500 dark:text-gray-400" align="left">Page url</th>
<th className="p-2 w-48 md:w-56 lg:w-1/3 text-xs tracking-wide font-bold text-gray-500 dark:text-gray-400" align="left">Page url</th>
<th className="p-2 w-32 text-xs tracking-wide font-bold text-gray-500 dark:text-gray-400" align="right">Unique Exits</th>
<th className="p-2 w-32 text-xs tracking-wide font-bold text-gray-500 dark:text-gray-400" align="right">Total Exits</th>
<th className="p-2 w-32 text-xs tracking-wide font-bold text-gray-500 dark:text-gray-400" align="right">Exit Rate</th>

View File

@ -81,11 +81,11 @@ class GoogleKeywordsModal extends React.Component {
}
} else if (this.state.searchTerms.length > 0) {
return (
<table className="w-full table-striped table-fixed">
<table className="w-max overflow-x-auto md:w-full table-striped table-fixed">
<thead>
<tr>
<th className="p-2 text-xs tracking-wide font-bold text-gray-500 dark:text-gray-400" align="left">Search Term</th>
<th className="p-2 w-32 text-xs tracking-wide font-bold text-gray-500 dark:text-gray-400" align="right">Visitors</th>
<th className="p-2 w-48 md:w-56 lg:w-1/3 text-xs tracking-wide font-bold text-gray-500 dark:text-gray-400" align="left">Search Term</th>
<th className="p-2 w-32 lg:w-1/2 text-xs tracking-wide font-bold text-gray-500 dark:text-gray-400" align="right">Visitors</th>
</tr>
</thead>
<tbody>

View File

@ -2,12 +2,22 @@ import React from "react";
import { createPortal } from "react-dom";
import { withRouter } from 'react-router-dom';
// This corresponds to the 'md' breakpoint on TailwindCSS.
const MD_WIDTH = 768;
// We assume that the dashboard is by default opened on a desktop. This is also a fall-back for when, for any reason, the width is not ascertained.
const DEFAULT_WIDTH = 1080;
class Modal extends React.Component {
constructor(props) {
super(props)
this.state = {
viewport: DEFAULT_WIDTH,
}
this.node = React.createRef()
this.handleClickOutside = this.handleClickOutside.bind(this)
this.handleKeyup = this.handleKeyup.bind(this)
this.handleResize = this.handleResize.bind(this)
}
componentDidMount() {
@ -15,6 +25,8 @@ class Modal extends React.Component {
document.body.style.height = '100vh';
document.addEventListener("mousedown", this.handleClickOutside);
document.addEventListener("keyup", this.handleKeyup);
window.addEventListener('resize', this.handleResize, false);
this.handleResize();
}
componentWillUnmount() {
@ -22,6 +34,7 @@ class Modal extends React.Component {
document.body.style.height = null;
document.removeEventListener("mousedown", this.handleClickOutside);
document.removeEventListener("keyup", this.handleKeyup);
window.removeEventListener('resize', this.handleResize, false);
}
handleClickOutside(e) {
@ -38,16 +51,43 @@ class Modal extends React.Component {
}
}
handleResize() {
this.setState({ viewport: window.innerWidth });
}
close() {
this.props.history.push(`/${encodeURIComponent(this.props.site.domain)}${this.props.location.search}`)
}
/**
* @description
* Decide whether to set max-width, and if so, to what.
* If no max-width is available, set width instead to min-content such that we can rely on widths set on th.
* On >md, we use the same behaviour as before: set width to 800 pixels.
* Note that When a max-width comes from the parent component, we rely on that *always*.
*/
getStyle() {
const { maxWidth } = this.props;
const { viewport } = this.state;
const styleObject = {};
if (maxWidth) {
styleObject.maxWidth = maxWidth;
} else {
styleObject.width = viewport <= MD_WIDTH ? "min-content" : "860px";
}
return styleObject;
}
render() {
return createPortal(
<div className="modal is-open" onClick={this.props.onClick}>
<div className="modal__overlay">
<button className="modal__close"></button>
<div ref={this.node} className="modal__container dark:bg-gray-800" style={{maxWidth: this.props.maxWidth || '860px'}}>
<div
ref={this.node}
className="modal__container dark:bg-gray-800"
style={this.getStyle()}
>
{this.props.children}
</div>

View File

@ -96,10 +96,10 @@ class PagesModal extends React.Component {
<div className="my-4 border-b border-gray-300"></div>
<main className="modal__content">
<table className="w-full table-striped table-fixed">
<table className="w-max overflow-x-auto md:w-full table-striped table-fixed">
<thead>
<tr>
<th className="p-2 text-xs tracking-wide font-bold text-gray-500 dark:text-gray-400" align="left">Page url</th>
<th className="p-2 w-48 md:w-56 lg:w-1/3 text-xs tracking-wide font-bold text-gray-500 dark:text-gray-400" align="left">Page url</th>
<th className="p-2 w-32 text-xs tracking-wide font-bold text-gray-500 dark:text-gray-400" align="right">{ this.label() }</th>
{this.showPageviews() && <th className="p-2 w-32 text-xs tracking-wide font-bold text-gray-500 dark:text-gray-400" align="right">Pageviews</th>}
{this.showExtra() && <th className="p-2 w-32 text-xs tracking-wide font-bold text-gray-500 dark:text-gray-400" align="right">Bounce rate</th>}

View File

@ -149,10 +149,10 @@ class ReferrerDrilldownModal extends React.Component {
<h1 className="text-xl font-semibold mb-0 leading-none dark:text-gray-200">{this.state.totalVisitors} visitors from {decodeURIComponent(this.props.match.params.referrer)}<br /> {toHuman(this.state.query)}</h1>
{this.renderGoalText()}
<table className="w-full table-striped table-fixed mt-4">
<table className="w-max overflow-x-auto md:w-full table-striped table-fixed mt-4">
<thead>
<tr>
<th className="p-2 text-xs tracking-wide font-bold text-gray-500 dark:text-gray-400" align="left">Referrer</th>
<th className="p-2 w-48 md:w-56 lg:w-1/3 text-xs tracking-wide font-bold text-gray-500 dark:text-gray-400" align="left">Referrer</th>
<th className="p-2 w-32 text-xs tracking-wide font-bold text-gray-500 dark:text-gray-400" align="right">Visitors</th>
{this.showExtra() && <th className="p-2 w-32 text-xs tracking-wide font-bold text-gray-500 dark:text-gray-400" align="right">Bounce rate</th>}
{this.showExtra() && <th className="p-2 w-32 text-xs tracking-wide font-bold text-gray-500 dark:text-gray-400" align="right">Visit duration</th>}

View File

@ -125,10 +125,10 @@ class SourcesModal extends React.Component {
<div className="my-4 border-b border-gray-300 dark:border-gray-500"></div>
<main className="modal__content">
<table className="w-full table-striped table-fixed">
<table className="w-max overflow-x-auto md:w-full table-striped table-fixed">
<thead>
<tr>
<th className="p-2 text-xs tracking-wide font-bold text-gray-500 dark:text-gray-400" align="left">Source</th>
<th className="p-2 w-48 md:w-56 lg:w-1/3 text-xs tracking-wide font-bold text-gray-500 dark:text-gray-400" align="left">Source</th>
<th className="p-2 w-32 text-xs tracking-wide font-bold text-gray-500 dark:text-gray-400" align="right">{this.label()}</th>
{this.showExtra() && <th className="p-2 w-32 text-xs tracking-wide font-bold text-gray-500 dark:text-gray-400" align="right">Bounce rate</th>}
{this.showExtra() && <th className="p-2 w-32 text-xs tracking-wide font-bold text-gray-500 dark:text-gray-400" align="right">Visit duration</th>}