Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add site switcher #281

Merged
merged 7 commits into from
Aug 13, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions assets/css/loader.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@
animation: loaderFadein .2s ease-in;
}

.loading.sm {
width: 25px;
height: 25px;
}

.loading div {
display: inline-block;
width: 50px;
Expand All @@ -15,6 +20,12 @@
-webkit-animation: spin 1s ease-in-out infinite;
}

.loading.sm div {
width: 25px;
height: 25px;
}


@keyframes spin {
to { -webkit-transform: rotate(360deg); }
}
Expand Down
3 changes: 2 additions & 1 deletion assets/js/dashboard/historical.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';

import Datepicker from './datepicker'
import SiteSwitcher from './site-switcher'
import Filters from './filters'
import CurrentVisitors from './stats/current-visitors'
import VisitorGraph from './stats/visitor-graph'
Expand All @@ -26,7 +27,7 @@ export default class Historical extends React.Component {
<div className="mb-12">
<div className="w-full sm:flex justify-between items-center">
<div className="w-full flex items-center">
<h2 className="text-left mr-8 font-semibold text-xl">Analytics for <a href={`http://${this.props.site.domain}`} target="_blank">{this.props.site.domain}</a></h2>
<SiteSwitcher site={this.props.site} loggedIn={this.props.loggedIn} />
<CurrentVisitors timer={this.props.timer} site={this.props.site} />
</div>
<Datepicker site={this.props.site} query={this.props.query} />
Expand Down
4 changes: 2 additions & 2 deletions assets/js/dashboard/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,9 @@ class Dashboard extends React.Component {

render() {
if (this.state.query.period === 'realtime') {
return <Realtime timer={this.state.timer} site={this.props.site} query={this.state.query} />
return <Realtime timer={this.state.timer} site={this.props.site} loggedIn={this.props.loggedIn} query={this.state.query} />
} else {
return <Historical timer={this.state.timer} site={this.props.site} query={this.state.query} />
return <Historical timer={this.state.timer} site={this.props.site} loggedIn={this.props.loggedIn} query={this.state.query} />
}
}
}
Expand Down
4 changes: 3 additions & 1 deletion assets/js/dashboard/mount.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ if (container) {
hasGoals: container.dataset.hasGoals === 'true'
}

const loggedIn = container.dataset.loggedIn === 'true'

const app = (
<ErrorBoundary>
<Router site={site} />
<Router site={site} loggedIn={loggedIn} />
</ErrorBoundary>
)

Expand Down
3 changes: 2 additions & 1 deletion assets/js/dashboard/realtime.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';

import Datepicker from './datepicker'
import SiteSwitcher from './site-switcher'
import Filters from './filters'
import CurrentVisitors from './stats/current-visitors'
import VisitorGraph from './stats/visitor-graph'
Expand All @@ -26,7 +27,7 @@ export default class Stats extends React.Component {
<div className="mb-12">
<div className="w-full sm:flex justify-between items-center">
<div className="w-full flex items-center">
<h2 className="text-left mr-8 font-semibold text-xl">Analytics for <a href={`http://${this.props.site.domain}`} target="_blank">{this.props.site.domain}</a></h2>
<SiteSwitcher site={this.props.site} loggedIn={this.props.loggedIn} />
</div>
<Datepicker site={this.props.site} query={this.props.query} />
</div>
Expand Down
4 changes: 2 additions & 2 deletions assets/js/dashboard/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,12 @@ function ScrollToTop() {
return null;
}

export default function Router({site}) {
export default function Router({site, loggedIn}) {
return (
<BrowserRouter>
<Route path="/:domain">
<ScrollToTop />
<Dash site={site} />
<Dash site={site} loggedIn={loggedIn} />
<Switch>
<Route exact path="/:domain/referrers">
<ReferrersModal site={site} />
Expand Down
120 changes: 120 additions & 0 deletions assets/js/dashboard/site-switcher.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import React from 'react';
import Transition from "../transition.js";

export default class SiteSwitcher extends React.Component {
constructor() {
super()
this.handleClick = this.handleClick.bind(this)
this.state = {
open: false,
sites: null,
error: null,
loading: true
}
}

componentDidMount() {
document.addEventListener('mousedown', this.handleClick, false);
}

componentWillUnmount() {
document.removeEventListener('mousedown', this.handleClick, false);
}

handleClick(e) {
if (this.dropDownNode && this.dropDownNode.contains(e.target)) return;
if (!this.state.open) return;

this.setState({open: false})
}

toggle() {
if (!this.props.loggedIn) return;

this.setState({open: !this.state.open})

if (!this.state.sites) {
fetch('/api/sites')
.then( response => {
if (!response.ok) { throw response }
return response.json()
})
.then((sites) => this.setState({loading: false, sites: sites}))
.catch((e) => this.setState({loading: false, error: e}))
}
}

renderSiteLink(domain) {
const extraClass = domain === this.props.site.domain ? 'font-medium text-gray-900' : 'hover:bg-gray-100 hover:text-gray-900 focus:outline-none focus:bg-gray-100 focus:text-gray-900'
return (
<a href={`/${encodeURIComponent(domain)}`} key={domain} className={`block truncate px-4 py-2 text-sm leading-5 text-gray-700 ${extraClass}`}>
<img src={`https://icons.duckduckgo.com/ip3/${domain}.ico`} className="inline w-4 mr-2 align-middle" />
<span>{domain}</span>
</a>
)
}

renderDropdown() {
if (this.state.loading) {
return <div className="px-4 py-6"><div className="loading sm mx-auto"><div></div></div></div>
} else if (this.state.error) {
return <div className="mx-auto px-4 py-6">Something went wrong, try again</div>
} else {
return (
<React.Fragment>
<div className="py-1">
<a href={`/${encodeURIComponent(this.props.site.domain)}/settings`} className="group flex items-center px-4 py-2 text-sm leading-5 text-gray-700 hover:bg-gray-100 hover:text-gray-900 focus:outline-none focus:bg-gray-100 focus:text-gray-900" role="menuitem">
<svg viewBox="0 0 20 20" fill="currentColor" className="mr-2 h-4 w-4 text-gray-500 group-hover:text-gray-600 group-focus:text-gray-500"><path d="M5 4a1 1 0 00-2 0v7.268a2 2 0 000 3.464V16a1 1 0 102 0v-1.268a2 2 0 000-3.464V4zM11 4a1 1 0 10-2 0v1.268a2 2 0 000 3.464V16a1 1 0 102 0V8.732a2 2 0 000-3.464V4zM16 3a1 1 0 011 1v7.268a2 2 0 010 3.464V16a1 1 0 11-2 0v-1.268a2 2 0 010-3.464V4a1 1 0 011-1z" /></svg>
Site settings
</a>
</div>
<div className="border-t border-gray-100"></div>
<div className="py-1">
{ this.state.sites.map(this.renderSiteLink.bind(this)) }
</div>
</React.Fragment>
)
}
}

renderArrow() {
if (this.props.loggedIn) {
return (
<svg className="-mr-1 ml-2 h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clipRule="evenodd" />
</svg>
)
}
}

render() {
const hoverClass = this.props.loggedIn ? 'hover:text-gray-500 focus:border-blue-300 focus:shadow-outline-blue ' : 'cursor-default'

return (
<div className="relative inline-block text-left z-10 mr-8">
<button onClick={this.toggle.bind(this)} className={`inline-flex items-center text-lg w-full rounded-md py-2 leading-5 font-bold text-gray-700 focus:outline-none transition ease-in-out duration-150 ${hoverClass}`}>

<img src={`https://icons.duckduckgo.com/ip3/${this.props.site.domain}.ico`} className="inline w-4 mr-2 align-middle" />
{this.props.site.domain}
{this.renderArrow()}
</button>

<Transition
show={this.state.open}
enter="transition ease-out duration-100 transform"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="transition ease-in duration-75 transform"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<div className="origin-top-left absolute left-0 mt-2 w-64 rounded-md shadow-lg" ref={node => this.dropDownNode = node} >
<div className="rounded-md bg-white shadow-xs">
{ this.renderDropdown() }
</div>
</div>
</Transition>
</div>
)
}
}
2 changes: 1 addition & 1 deletion assets/js/dashboard/stats/current-visitors.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export default class CurrentVisitors extends React.Component {
const { currentVisitors } = this.state;
if (currentVisitors !== null) {
return (
<Link to={`/${encodeURIComponent(this.props.site.domain)}?period=realtime`} className="block text-sm font-bold text-gray-500 mt-1">
<Link to={`/${encodeURIComponent(this.props.site.domain)}?period=realtime`} className="block text-sm font-bold text-gray-500">
<svg className="w-2 mr-2 fill-current text-green-500 inline" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<circle cx="8" cy="8" r="8"/>
</svg>
Expand Down
107 changes: 107 additions & 0 deletions assets/js/transition.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// https://gist.github.com/adamwathan/3b9f3ad1a285a2d1b482769aeb862467
import { CSSTransition as ReactCSSTransition } from 'react-transition-group'
import React, { useRef, useEffect, useContext } from 'react'

const TransitionContext = React.createContext({
parent: {},
})

function useIsInitialRender() {
const isInitialRender = useRef(true)
useEffect(() => {
isInitialRender.current = false
}, [])
return isInitialRender.current
}

function CSSTransition({
show,
enter = '',
enterFrom = '',
enterTo = '',
leave = '',
leaveFrom = '',
leaveTo = '',
appear,
children,
}) {
const enterClasses = enter.split(' ').filter((s) => s.length)
const enterFromClasses = enterFrom.split(' ').filter((s) => s.length)
const enterToClasses = enterTo.split(' ').filter((s) => s.length)
const leaveClasses = leave.split(' ').filter((s) => s.length)
const leaveFromClasses = leaveFrom.split(' ').filter((s) => s.length)
const leaveToClasses = leaveTo.split(' ').filter((s) => s.length)

function addClasses(node, classes) {
classes.length && node.classList.add(...classes)
}

function removeClasses(node, classes) {
classes.length && node.classList.remove(...classes)
}

return (
<ReactCSSTransition
appear={appear}
unmountOnExit
in={show}
addEndListener={(node, done) => {
node.addEventListener('transitionend', done, false)
}}
onEnter={(node) => {
addClasses(node, [...enterClasses, ...enterFromClasses])
}}
onEntering={(node) => {
removeClasses(node, enterFromClasses)
addClasses(node, enterToClasses)
}}
onEntered={(node) => {
removeClasses(node, [...enterToClasses, ...enterClasses])
}}
onExit={(node) => {
addClasses(node, [...leaveClasses, ...leaveFromClasses])
}}
onExiting={(node) => {
removeClasses(node, leaveFromClasses)
addClasses(node, leaveToClasses)
}}
onExited={(node) => {
removeClasses(node, [...leaveToClasses, ...leaveClasses])
}}
>
{children}
</ReactCSSTransition>
)
}

function Transition({ show, appear, ...rest }) {
const { parent } = useContext(TransitionContext)
const isInitialRender = useIsInitialRender()
const isChild = show === undefined

if (isChild) {
return (
<CSSTransition
appear={parent.appear || !parent.isInitialRender}
show={parent.show}
{...rest}
/>
)
}

return (
<TransitionContext.Provider
value={{
parent: {
show,
isInitialRender,
appear,
},
}}
>
<CSSTransition appear={appear} show={show} {...rest} />
</TransitionContext.Provider>
)
}

export default Transition
Loading