Skip to content

Commit

Permalink
Adds dark mode to entire dashboard (#467)
Browse files Browse the repository at this point in the history
* Adds New Dark Mode Assets

* Moves triangle for dropdown to a reasonable position

* Majority .eex dark implementation

* Fixes Logo Positioning

* Adds theme flag to user schema, uses it

* Uses correct variables for theme applicator script

* Minor missed theme changes/fallbacks

* Individual Component Support + Theme Context

* Sources Tab Support

This was a pain to test D:

* Partial Stats Sections Support

* More of stats modules supported

* Modal +table support

* Improves some Flatpickr in light theme, supports dark theme

* Fixes missed settings tab colors

* Finishes Devices module support

* Fixes bar graph colors

* Better colorizes maps module

* Undoes colorized bars

(they looked bad, on second thought)

* Fixes loading indicator

* Finishes conversions module

* Adds changelog entry

The PR number could be wrong, will double check

* Fixes missed header color

* Fixes naming of migration and removes static alter

* Does migration correctly

As I said, my Elixir is pretty weak heh

* Adds support for spike notifications setting

* Improves contrast and visibility for email settings

* Resolves @ukutaht's comments on #467

* Fixes missing dark style

* Found one more missed dark element (shared links)

* Formatting fixes
  • Loading branch information
Vigasaurus committed Dec 16, 2020
1 parent a50fd55 commit 425975e
Show file tree
Hide file tree
Showing 81 changed files with 884 additions and 562 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ All notable changes to this project will be documented in this file.
- Display weekday on the visitor graph plausible/analytics#175
- Collect and display browser & OS versions plausible/analytics#397
- Simple notifications around traffic spikes plausible/analytics#453
- Dark theme option/system setting follow plausible/analytics#467

### Changed
- Use alpine as base image to decrease Docker image size plausible/analytics#353
Expand Down
18 changes: 16 additions & 2 deletions assets/css/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
@import "modal.css";
@import "loader.css";
@import "tooltip.css";
@import "flatpickr.dark.css";

.button {
@apply bg-indigo-600 border border-transparent rounded-md py-2 px-4 inline-flex justify-center text-sm leading-5 font-medium text-white transition hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500;
Expand Down Expand Up @@ -156,7 +157,7 @@ blockquote {

.dropdown-content::before {
top: -16px;
right: 64px;
right: 8px;
left: auto;
}
.dropdown-content::before {
Expand All @@ -174,7 +175,7 @@ blockquote {
}
.dropdown-content::after {
top: -14px;
right: 65px;
right: 9px;
left: auto;
}

Expand All @@ -196,6 +197,14 @@ blockquote {
background-color: #f1f5f8;
}

.dark .table-striped tbody tr:nth-child(odd) {
background-color: rgb(37, 47, 63);
}

.dark .table-striped tbody tr:nth-child(even) {
background-color: rgb(26, 32, 44);
}

.twitter-icon {
width: 1.25em;
height: 1.25em;
Expand Down Expand Up @@ -252,3 +261,8 @@ blockquote {
.datamaps-subunit {
cursor: pointer;
}

/* Only because the map handler doesn't expose an easier way to change the shadow color */
.dark .hoverinfo {
box-shadow: 1px 1px 5px rgb(26, 32, 44);
}
110 changes: 110 additions & 0 deletions assets/css/flatpickr.dark.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/* Because Flatpickr offers zero support for dynamic theming on its own (outside of third-party plugins) */
.dark .flatpickr-calendar {
background-color: #1f2937;
}

.dark .flatpickr-weekday {
color: #f3f4f6;
}

.dark .flatpickr-prev-month {
fill: #f3f4f6 !important;
}

.dark .flatpickr-next-month {
fill: #f3f4f6 !important;
}

.dark .flatpickr-monthDropdown-months {
color: #f3f4f6 !important;
}

.dark .numInputWrapper {
color: #f3f4f6;
}

.dark .flatpickr-day.prevMonthDay {
color: #94a3af;
}

.dark .flatpickr-day {
color: #E5E7EB;
}

.dark .flatpickr-day.prevMonthDay {
color: #9CA3AF;
}

.dark .flatpickr-day.nextMonthDay {
color: #9CA3AF;
}

.dark .flatpickr-day:hover {
background-color: #374151;
}

.dark :not(.startRange):not(.endRange).flatpickr-day.nextMonthDay:hover {
background-color: #374151;
}

.dark :not(.startRange):not(.endRange).flatpickr-day.prevMonthDay:hover {
background-color: #374151;
}

.dark .flatpickr-next-month {
fill: #f3f4f6;
}

.dark .flatpickr-day.flatpickr-disabled {
color: #4B5563;
}

.dark .flatpickr-day.flatpickr-disabled:hover {
color: #4B5563;
}

.dark .flatpickr-day.today {
background-color: rgba(167, 243, 208, 0.5);
}

.dark .flatpickr-day.today {
border-color: #34D399;
}

.dark .flatpickr-day.inRange {
background-color: #374151;
box-shadow: -5px 0 0 #374151,5px 0 0 #374151;
border-color: #374151;
}

.dark .flatpickr-day.prevMonthDay.inRange {
background-color: #374151;
box-shadow: -5px 0 0 #374151,5px 0 0 #374151;
border-color: #374151;
}

.dark .flatpickr-day.nextMonthDay.inRange {
background-color: #374151;
box-shadow: -5px 0 0 #374151,5px 0 0 #374151;
border-color: #374151;
}

.flatpickr-day.startRange {
background: #6574cd !important;
border-color: #6574cd !important;
}

.flatpickr-day.endRange {
background: #6574cd !important;
border-color: #6574cd !important;
}

.dark .flatpickr-day.selected.startRange + .endRange:not(:nth-child(7n+1)), .flatpickr-day.startRange.startRange + .endRange:not(:nth-child(7n+1)), .flatpickr-day.endRange.startRange + .endRange:not(:nth-child(7n+1)) {
-webkit-box-shadow: -10px 0 0 #4556c3 !important;
box-shadow: -10px 0 0 #4556c3 !important;
}

.flatpickr-day.selected.startRange + .endRange:not(:nth-child(7n+1)), .flatpickr-day.startRange.startRange + .endRange:not(:nth-child(7n+1)), .flatpickr-day.endRange.startRange + .endRange:not(:nth-child(7n+1)) {
-webkit-box-shadow: -10px 0 0 #4556c3 !important;
box-shadow: -10px 0 0 #4556c3 !important;
}
5 changes: 5 additions & 0 deletions assets/css/loader.css
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@
-webkit-animation: spin 1s ease-in-out infinite;
}

.dark .loading div {
border: 3px solid #606f7b;
border-top-color: #dae1e7;
}

.loading.sm div {
width: 25px;
height: 25px;
Expand Down
22 changes: 11 additions & 11 deletions assets/js/dashboard/datepicker.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,11 +101,11 @@ class DatePicker extends React.Component {

renderArrow(period, prevDate, nextDate) {
return (
<div className="flex rounded shadow bg-white mr-4 cursor-pointer">
<QueryLink to={{date: prevDate}} query={this.props.query} className="flex items-center px-2 border-r border-gray-300">
<div className="flex rounded shadow bg-white dark:bg-gray-800 mr-4 cursor-pointer">
<QueryLink to={{date: prevDate}} query={this.props.query} className="flex items-center px-2 border-r border-gray-300 dark:border-gray-500 dark:text-gray-100">
<svg className="feather h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="15 18 9 12 15 6"></polyline></svg>
</QueryLink>
<QueryLink to={{date: nextDate}} query={this.props.query} className="flex items-center px-2">
<QueryLink to={{date: nextDate}} query={this.props.query} className="flex items-center px-2 dark:text-gray-100">
<svg className="feather h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg>
</QueryLink>
</div>
Expand Down Expand Up @@ -135,7 +135,7 @@ class DatePicker extends React.Component {
renderDropDown() {
return (
<div className="relative" style={{height: '35.5px', width: '190px'}} ref={node => this.dropDownNode = node}>
<div onClick={this.open.bind(this)} className="flex items-center justify-between rounded bg-white shadow px-4 pr-3 py-2 leading-tight cursor-pointer text-sm font-medium text-gray-800 h-full">
<div onClick={this.open.bind(this)} className="flex items-center justify-between rounded bg-white dark:bg-gray-800 shadow px-4 pr-3 py-2 leading-tight cursor-pointer text-sm font-medium text-gray-800 dark:text-gray-200 h-full">
<span className="mr-2">{this.timeFrameText()}</span>
<svg className="text-pink-500 h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="6 9 12 15 18 9"></polyline>
Expand Down Expand Up @@ -176,7 +176,7 @@ class DatePicker extends React.Component {
if (opts.date) { opts.date = formatISO(opts.date) }

return (
<QueryLink to={{period, ...opts}} onClick={this.close.bind(this)} query={this.props.query} className={boldClass + ' block px-4 py-2 text-sm leading-tight hover:bg-gray-100 hover:text-gray-900'}>
<QueryLink to={{period, ...opts}} onClick={this.close.bind(this)} query={this.props.query} className={boldClass + ' block px-4 py-2 text-sm leading-tight hover:bg-gray-100 dark:hover:bg-gray-900 hover:text-gray-900 dark:hover:text-gray-100'}>
{text}
</QueryLink>
)
Expand All @@ -186,29 +186,29 @@ class DatePicker extends React.Component {
if (this.state.mode === 'menu') {
return (
<div className="absolute mt-2 rounded shadow-md z-10" style={{width: '235px', right: '-14px'}}>
<div className="rounded bg-white ring-1 ring-black ring-opacity-5 font-medium text-gray-800">
<div className="rounded bg-white dark:bg-gray-800 ring-1 ring-black ring-opacity-5 font-medium text-gray-800 dark:text-gray-200">
<div className="py-1">
{ this.renderLink('day', 'Today') }
{ this.renderLink('realtime', 'Realtime') }
</div>
<div className="border-t border-gray-200"></div>
<div className="border-t border-gray-200 dark:border-gray-500"></div>
<div className="py-1">
{ this.renderLink('7d', 'Last 7 days') }
{ this.renderLink('30d', 'Last 30 days') }
</div>
<div className="border-t border-gray-200"></div>
<div className="border-t border-gray-200 dark:border-gray-500"></div>
<div className="py-1">
{ this.renderLink('month', 'This month') }
{ this.renderLink('month', 'Last month', {date: lastMonth(this.props.site)}) }
</div>
<div className="border-t border-gray-200"></div>
<div className="border-t border-gray-200 dark:border-gray-500"></div>
<div className="py-1">
{ this.renderLink('6mo', 'Last 6 months') }
{ this.renderLink('12mo', 'Last 12 months') }
</div>
<div className="border-t border-gray-200"></div>
<div className="border-t border-gray-200 dark:border-gray-500"></div>
<div className="py-1">
<span onClick={e => this.setState({mode: 'calendar'}, this.openCalendar.bind(this))} className="block px-4 py-2 text-sm leading-tight hover:bg-gray-100 hover:text-gray-900 cursor-pointer">Custom range</span>
<span onClick={e => this.setState({mode: 'calendar'}, this.openCalendar.bind(this))} className="block px-4 py-2 text-sm leading-tight hover:bg-gray-100 dark:hover:bg-gray-900 hover:text-gray-900 dark:hover:text-gray-100 cursor-pointer">Custom range</span>
</div>
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion assets/js/dashboard/error-boundary.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export default class ErrorBoundary extends React.Component {
render() {
if (this.state.error) {
return (
<div className="text-center text-gray-900 mt-36">
<div className="text-center text-gray-900 dark:text-gray-100 mt-36">
<RocketIcon />
<div className="text-lg font-bold">Oops! Something went wrong</div>
<div className="text-lg">{this.state.error.name + ': ' + this.state.error.message}</div>
Expand Down
2 changes: 1 addition & 1 deletion assets/js/dashboard/filters.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ function renderFilter(history, [key, value], query) {
}

return (
<span key={key} title={value} className="inline-flex bg-white text-gray-700 shadow text-sm rounded py-2 px-3 mr-4">
<span key={key} title={value} className="inline-flex bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 shadow text-sm rounded py-2 px-3 mr-4">
{filterText(key, value, query)} <b className="ml-1 cursor-pointer" onClick={removeFilter}></b>
</span>
)
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 @@ -5,7 +5,7 @@ import Historical from './historical'
import Realtime from './realtime'
import {parseQuery} from './query'
import * as api from './api'

import { withThemeProvider } from './theme-provider-hoc';

const THIRTY_SECONDS = 30000

Expand Down Expand Up @@ -51,4 +51,4 @@ class Dashboard extends React.Component {
}
}

export default withRouter(Dashboard)
export default withRouter(withThemeProvider(Dashboard))
18 changes: 9 additions & 9 deletions assets/js/dashboard/site-switcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@ export default class SiteSwitcher extends React.Component {
}

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'
const extraClass = domain === this.props.site.domain ? 'font-medium text-gray-900 dark:text-gray-100' : 'hover:bg-gray-100 dark:hover:bg-gray-900 hover:text-gray-900 dark:hover:text-gray-100 focus:outline-none focus:bg-gray-100 dark:focus:bg-gray-900 focus:text-gray-900 dark:focus:text-gray-100'
return (
<a href={`/${encodeURIComponent(domain)}`} key={domain} className={`block truncate px-4 py-2 text-sm leading-5 text-gray-700 ${extraClass}`}>
<a href={`/${encodeURIComponent(domain)}`} key={domain} className={`block truncate px-4 py-2 text-sm leading-5 text-gray-700 dark:text-gray-300 ${extraClass}`}>
<img src={`https://icons.duckduckgo.com/ip3/${domain}.ico`} referrerPolicy="no-referrer" className="inline w-4 mr-2 align-middle" />
<span>{domain}</span>
</a>
Expand All @@ -58,17 +58,17 @@ export default class SiteSwitcher extends React.Component {
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>
return <div className="mx-auto px-4 py-6 dark:text-gray-100">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>
<a href={`/${encodeURIComponent(this.props.site.domain)}/settings`} className="group flex items-center px-4 py-2 text-sm leading-5 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-900 hover:text-gray-900 dark:hover:text-gray-100 focus:outline-none focus:bg-gray-100 dark:focus:bg-gray-900 focus:text-gray-900 dark:focus:text-gray-100" role="menuitem">
<svg viewBox="0 0 20 20" fill="currentColor" className="mr-2 h-4 w-4 text-gray-500 dark:text-gray-200 group-hover:text-gray-600 dark:group-hover:text-gray-400 group-focus:text-gray-500 dark:group-focus:text-gray-200"><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="border-t border-gray-100 dark:border-gray-900"></div>
<div className="py-1">
{ this.state.sites.map(this.renderSiteLink.bind(this)) }
</div>
Expand All @@ -88,11 +88,11 @@ export default class SiteSwitcher extends React.Component {
}

render() {
const hoverClass = this.props.loggedIn ? 'hover:text-gray-500 focus:border-blue-300 focus:ring ' : 'cursor-default'
const hoverClass = this.props.loggedIn ? 'hover:text-gray-500 dark:hover:text-gray-200 focus:border-blue-300 focus:ring ' : '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}`}>
<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 dark:text-gray-300 focus:outline-none transition ease-in-out duration-150 ${hoverClass}`}>

<img src={`https://icons.duckduckgo.com/ip3/${this.props.site.domain}.ico`} referrerPolicy="no-referrer" className="inline w-4 mr-2 align-middle" />
{this.props.site.domain}
Expand All @@ -109,7 +109,7 @@ export default class SiteSwitcher extends React.Component {
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 ring-1 ring-black ring-opacity-5">
<div className="rounded-md bg-white dark:bg-gray-800 ring-1 ring-black ring-opacity-5">
{ this.renderDropdown() }
</div>
</div>
Expand Down
14 changes: 7 additions & 7 deletions assets/js/dashboard/stats/conversions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,11 @@ export default class Conversions extends React.Component {
return (
<div className="my-2 text-sm" key={goal.name}>
<div className="flex items-center justify-between my-2">
<div className="w-full h-8 relative" style={{maxWidth: 'calc(100% - 16rem)'}}>
<Bar count={goal.count} all={this.state.goals} bg="bg-red-50" />
<div className="w-full h-8 relative dark:text-gray-300" style={{maxWidth: 'calc(100% - 16rem)'}}>
<Bar count={goal.count} all={this.state.goals} bg="bg-red-50 dark:bg-gray-500 dark:bg-opacity-15" />
{this.renderGoalText(goal.name)}
</div>
<div>
<div className="dark:text-gray-200">
<span className="font-medium inline-block w-20 text-right">{numberFormatter(goal.count)}</span>
<span className="font-medium inline-block w-20 text-right">{numberFormatter(goal.total_count)}</span>
<span className="font-medium inline-block w-20 text-right">{goal.conversion_rate}%</span>
Expand All @@ -68,15 +68,15 @@ export default class Conversions extends React.Component {
render() {
if (this.state.loading) {
return (
<div className="w-full bg-white shadow-xl rounded p-4" style={{height: '94px'}}>
<div className="w-full bg-white dark:bg-gray-825 shadow-xl rounded p-4" style={{height: '94px'}}>
<div className="loading my-2 mx-auto"><div></div></div>
</div>
)
} else if (this.state.goals) {
return (
<div className="w-full bg-white shadow-xl rounded p-4">
<h3 className="font-bold">{this.props.title || "Goal Conversions"}</h3>
<div className="flex items-center mt-3 mb-2 justify-between text-gray-500 text-xs font-bold tracking-wide">
<div className="w-full bg-white dark:bg-gray-825 shadow-xl rounded p-4">
<h3 className="font-bold dark:text-gray-100">{this.props.title || "Goal Conversions"}</h3>
<div className="flex items-center mt-3 mb-2 justify-between text-gray-500 dark:text-gray-400 text-xs font-bold tracking-wide">
<span>Goal</span>
<div className="text-right">
<span className="inline-block w-20">Uniques</span>
Expand Down

0 comments on commit 425975e

Please sign in to comment.