mirror of
https://github.com/plausible/analytics.git
synced 2024-12-21 00:21:43 +03:00
9ce9264144
* 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. * Improve bar component to work in a variety of situations This fixes two issues from #972 - Goals area has display issues depending on the name of your custom events - It is not possible to view labels for outbound links, 404 and custom props * Update changelog entry * Move content to children for Bar component * Remove redundant fallback width * Fix text color on root conversion texts Hyperlinks (<a>) inherit their color. In the case of the root conversion text, we needed to specify the color somewhere. The same color as the breakdown texts has been chosen. The Bar component no longer needs to take in text classes.
226 lines
7.0 KiB
JavaScript
226 lines
7.0 KiB
JavaScript
import React from 'react';
|
|
import { Link } from 'react-router-dom'
|
|
|
|
import * as storage from '../../storage'
|
|
import LazyLoader from '../../lazy-loader'
|
|
import Browsers from './browsers'
|
|
import OperatingSystems from './operating-systems'
|
|
import FadeIn from '../../fade-in'
|
|
import numberFormatter from '../../number-formatter'
|
|
import Bar from '../bar'
|
|
import * as api from '../../api'
|
|
|
|
|
|
const EXPLANATION = {
|
|
'Mobile': 'up to 576px',
|
|
'Tablet': '576px to 992px',
|
|
'Laptop': '992px to 1440px',
|
|
'Desktop': 'above 1440px',
|
|
}
|
|
|
|
function iconFor(screenSize) {
|
|
if (screenSize === 'Mobile') {
|
|
return (
|
|
<svg width="16px" height="16px" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="-mt-px feather"><rect x="5" y="2" width="14" height="20" rx="2" ry="2"/><line x1="12" y1="18" x2="12" y2="18"/></svg>
|
|
)
|
|
} else if (screenSize === 'Tablet') {
|
|
return (
|
|
<svg width="16px" height="16px" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="-mt-px feather"><rect x="4" y="2" width="16" height="20" rx="2" ry="2" transform="rotate(180 12 12)"/><line x1="12" y1="18" x2="12" y2="18"/></svg>
|
|
)
|
|
} else if (screenSize === 'Laptop') {
|
|
return (
|
|
<svg width="16px" height="16px" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="-mt-px feather"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="2" y1="20" x2="22" y2="20"/></svg>
|
|
)
|
|
} else if (screenSize === 'Desktop') {
|
|
return (
|
|
<svg width="16px" height="16px" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="-mt-px feather"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>
|
|
)
|
|
}
|
|
}
|
|
|
|
class ScreenSizes extends React.Component {
|
|
constructor(props) {
|
|
super(props)
|
|
this.state = {loading: true}
|
|
this.onVisible = this.onVisible.bind(this)
|
|
}
|
|
|
|
componentDidUpdate(prevProps) {
|
|
if (this.props.query !== prevProps.query) {
|
|
this.setState({loading: true, sizes: null})
|
|
this.fetchScreenSizes()
|
|
}
|
|
}
|
|
|
|
onVisible() {
|
|
this.fetchScreenSizes()
|
|
if (this.props.timer) this.props.timer.onTick(this.fetchScreenSizes.bind(this))
|
|
}
|
|
|
|
|
|
fetchScreenSizes() {
|
|
api.get(
|
|
`/api/stats/${encodeURIComponent(this.props.site.domain)}/screen-sizes`,
|
|
this.props.query
|
|
)
|
|
.then((res) => this.setState({loading: false, sizes: res}))
|
|
}
|
|
|
|
label() {
|
|
return this.props.query.period === 'realtime' ? 'Current visitors' : 'Visitors'
|
|
}
|
|
|
|
renderScreenSize(size) {
|
|
const query = new URLSearchParams(window.location.search)
|
|
query.set('screen', size.name)
|
|
|
|
return (
|
|
<div className="flex items-center justify-between my-1 text-sm" key={size.name}>
|
|
<Bar
|
|
count={size.count}
|
|
all={this.state.sizes}
|
|
bg="bg-green-50 dark:bg-gray-500 dark:bg-opacity-15"
|
|
maxWidthDeduction="6rem"
|
|
>
|
|
<span
|
|
tooltip={EXPLANATION[size.name]}
|
|
className="flex px-2 py-1.5 dark:text-gray-300"
|
|
>
|
|
<Link className="block hover:underline" to={{search: query.toString()}}>
|
|
{iconFor(size.name)} {size.name}
|
|
</Link>
|
|
</span>
|
|
</Bar>
|
|
<span
|
|
className="font-medium dark:text-gray-200"
|
|
>
|
|
{numberFormatter(size.count)}
|
|
<span className="inline-block w-8 text-xs text-right">({size.percentage}%)</span>
|
|
</span>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
renderList() {
|
|
if (this.state.sizes && this.state.sizes.length > 0) {
|
|
return (
|
|
<React.Fragment>
|
|
<div
|
|
className="flex items-center justify-between mt-3 mb-2 text-xs font-bold tracking-wide text-gray-500"
|
|
>
|
|
<span>Screen size</span>
|
|
<span>{ this.label() }</span>
|
|
</div>
|
|
{ this.state.sizes && this.state.sizes.map(this.renderScreenSize.bind(this)) }
|
|
</React.Fragment>
|
|
)
|
|
}
|
|
return (
|
|
<div
|
|
className="font-medium text-center text-gray-500 mt-44 dark:text-gray-400"
|
|
>
|
|
No data yet
|
|
</div>
|
|
)
|
|
}
|
|
|
|
render() {
|
|
return (
|
|
<LazyLoader onVisible={this.onVisible} className="flex flex-col flex-grow">
|
|
{ this.state.loading && <div className="mx-auto loading mt-44"><div></div></div> }
|
|
<FadeIn show={!this.state.loading} class="flex-grow">
|
|
{ this.renderList() }
|
|
</FadeIn>
|
|
</LazyLoader>
|
|
)
|
|
}
|
|
}
|
|
|
|
export default class Devices extends React.Component {
|
|
constructor(props) {
|
|
super(props)
|
|
this.tabKey = `deviceTab__${ props.site.domain}`
|
|
const storedTab = storage.getItem(this.tabKey)
|
|
this.state = {
|
|
mode: storedTab || 'size'
|
|
}
|
|
}
|
|
|
|
|
|
setMode(mode) {
|
|
return () => {
|
|
storage.setItem(this.tabKey, mode)
|
|
this.setState({mode})
|
|
}
|
|
}
|
|
|
|
renderContent() {
|
|
switch (this.state.mode) {
|
|
case 'browser':
|
|
return <Browsers site={this.props.site} query={this.props.query} timer={this.props.timer} />
|
|
case 'os':
|
|
return (
|
|
<OperatingSystems
|
|
site={this.props.site}
|
|
query={this.props.query}
|
|
timer={this.props.timer}
|
|
/>
|
|
)
|
|
case 'size':
|
|
default:
|
|
return (
|
|
<ScreenSizes
|
|
site={this.props.site}
|
|
query={this.props.query}
|
|
timer={this.props.timer}
|
|
/>
|
|
)
|
|
}
|
|
}
|
|
|
|
renderPill(name, mode) {
|
|
const isActive = this.state.mode === mode
|
|
|
|
if (isActive) {
|
|
return (
|
|
<li
|
|
className="inline-block h-5 font-bold text-indigo-700 border-b-2 border-indigo-700 dark:text-indigo-500 dark:border-indigo-500"
|
|
>
|
|
{name}
|
|
</li>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<li
|
|
className="cursor-pointer hover:text-indigo-600"
|
|
onClick={this.setMode(mode)}
|
|
>
|
|
{name}
|
|
</li>
|
|
)
|
|
}
|
|
|
|
render() {
|
|
return (
|
|
<div
|
|
className="stats-item flex flex-col mt-6 stats-item--has-header w-full"
|
|
>
|
|
<div
|
|
className="stats-item__header flex flex-col flex-grow relative p-4 bg-white rounded shadow-xl dark:bg-gray-825"
|
|
>
|
|
<div className="flex justify-between w-full">
|
|
<h3 className="font-bold dark:text-gray-100">Devices</h3>
|
|
<ul className="flex text-xs font-medium text-gray-500 dark:text-gray-400 space-x-2">
|
|
{ this.renderPill('Size', 'size') }
|
|
{ this.renderPill('Browser', 'browser') }
|
|
{ this.renderPill('OS', 'os') }
|
|
</ul>
|
|
</div>
|
|
{ this.renderContent() }
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
}
|