mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2024-12-23 17:40:23 +03:00
Merge pull request #391 from acelaya-forks/feature/dark-theme
Feature/dark theme
This commit is contained in:
commit
f653739d50
41 changed files with 501 additions and 97 deletions
|
@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
* [#379](https://github.com/shlinkio/shlink-web-client/issues/379) and [#384](https://github.com/shlinkio/shlink-web-client/issues/384) Improved QR code modal, including controls to customize size, format and margin, as well as a button to copy the link to the clipboard.
|
* [#379](https://github.com/shlinkio/shlink-web-client/issues/379) and [#384](https://github.com/shlinkio/shlink-web-client/issues/384) Improved QR code modal, including controls to customize size, format and margin, as well as a button to copy the link to the clipboard.
|
||||||
* [#385](https://github.com/shlinkio/shlink-web-client/issues/385) Added setting to determine if "validate URL" should be enabled or disabled by default.
|
* [#385](https://github.com/shlinkio/shlink-web-client/issues/385) Added setting to determine if "validate URL" should be enabled or disabled by default.
|
||||||
* [#386](https://github.com/shlinkio/shlink-web-client/issues/386) Added new card in overview section to display amount of orphan visits when using Shlink 2.6.0 or higher.
|
* [#386](https://github.com/shlinkio/shlink-web-client/issues/386) Added new card in overview section to display amount of orphan visits when using Shlink 2.6.0 or higher.
|
||||||
|
* [#177](https://github.com/shlinkio/shlink-web-client/issues/177) Added dark theme.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
* *Nothing*
|
* *Nothing*
|
||||||
|
|
|
@ -2,11 +2,14 @@ import { useEffect, FC } from 'react';
|
||||||
import { Route, Switch } from 'react-router-dom';
|
import { Route, Switch } from 'react-router-dom';
|
||||||
import NotFound from './common/NotFound';
|
import NotFound from './common/NotFound';
|
||||||
import { ServersMap } from './servers/data';
|
import { ServersMap } from './servers/data';
|
||||||
|
import { Settings } from './settings/reducers/settings';
|
||||||
|
import { changeThemeInMarkup } from './utils/theme';
|
||||||
import './App.scss';
|
import './App.scss';
|
||||||
|
|
||||||
interface AppProps {
|
interface AppProps {
|
||||||
fetchServers: Function;
|
fetchServers: Function;
|
||||||
servers: ServersMap;
|
servers: ServersMap;
|
||||||
|
settings: Settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
const App = (
|
const App = (
|
||||||
|
@ -17,12 +20,14 @@ const App = (
|
||||||
EditServer: FC,
|
EditServer: FC,
|
||||||
Settings: FC,
|
Settings: FC,
|
||||||
ShlinkVersionsContainer: FC,
|
ShlinkVersionsContainer: FC,
|
||||||
) => ({ fetchServers, servers }: AppProps) => {
|
) => ({ fetchServers, servers, settings }: AppProps) => {
|
||||||
// On first load, try to fetch the remote servers if the list is empty
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// On first load, try to fetch the remote servers if the list is empty
|
||||||
if (Object.keys(servers).length === 0) {
|
if (Object.keys(servers).length === 0) {
|
||||||
fetchServers();
|
fetchServers();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
changeThemeInMarkup(settings.ui?.theme ?? 'light');
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
.aside-menu {
|
.aside-menu {
|
||||||
width: $asideMenuWidth;
|
width: $asideMenuWidth;
|
||||||
background-color: white;
|
background-color: var(--primary-color);
|
||||||
box-shadow: rgba(0, 0, 0, .05) 0 8px 15px;
|
box-shadow: rgba(0, 0, 0, .05) 0 8px 15px;
|
||||||
position: fixed !important;
|
position: fixed !important;
|
||||||
padding-top: 13px;
|
padding-top: 13px;
|
||||||
|
@ -18,7 +18,6 @@
|
||||||
|
|
||||||
@media (min-width: $mdMin) {
|
@media (min-width: $mdMin) {
|
||||||
padding: 30px 15px 15px;
|
padding: 30px 15px 15px;
|
||||||
border-right: 1px solid #eeeeee;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: $smMax) {
|
@media (max-width: $smMax) {
|
||||||
|
@ -50,17 +49,13 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.aside-menu__item:hover {
|
.aside-menu__item:hover {
|
||||||
background-color: $lightColor;
|
background-color: var(--secondary-color);
|
||||||
}
|
|
||||||
|
|
||||||
.aside-menu__item--selected {
|
|
||||||
color: #ffffff;
|
|
||||||
background-color: $mainColor;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.aside-menu__item--selected,
|
||||||
.aside-menu__item--selected:hover {
|
.aside-menu__item--selected:hover {
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
background-color: $mainColor;
|
background-color: var(--brand-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.aside-menu__item--divider {
|
.aside-menu__item--divider {
|
||||||
|
|
|
@ -36,6 +36,6 @@
|
||||||
|
|
||||||
.home__servers-container {
|
.home__servers-container {
|
||||||
@media (min-width: $mdMin) {
|
@media (min-width: $mdMin) {
|
||||||
border-left: 1px solid rgba(0, 0, 0, .125);
|
border-left: 1px solid var(--border-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
@import '../utils/base';
|
@import '../utils/base';
|
||||||
|
|
||||||
.main-header.main-header {
|
.main-header.main-header {
|
||||||
background-color: $mainColor !important;
|
|
||||||
color: white;
|
color: white;
|
||||||
|
background-color: var(--brand-color) !important;
|
||||||
|
|
||||||
.navbar-brand {
|
.navbar-brand {
|
||||||
color: inherit !important;
|
color: inherit !important;
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
|
@import '../utils/base';
|
||||||
|
|
||||||
.react-tagsinput {
|
.react-tagsinput {
|
||||||
background-color: #ffffff;
|
background-color: var(--input-color);
|
||||||
border: 1px solid #cccccc;
|
border: 1px solid var(--input-border-color);
|
||||||
border-radius: .25rem;
|
border-radius: .25rem;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
min-height: 2.6rem;
|
min-height: 2.6rem;
|
||||||
|
@ -10,7 +12,7 @@
|
||||||
|
|
||||||
.react-tagsinput--focused {
|
.react-tagsinput--focused {
|
||||||
border-color: #80bdff;
|
border-color: #80bdff;
|
||||||
box-shadow: 0 0 0 .2rem rgba(0, 123, 255, .25);
|
box-shadow: 0 0 0 .2rem rgb(70 150 229 / 25%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-tagsinput-tag {
|
.react-tagsinput-tag {
|
||||||
|
@ -44,5 +46,13 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-bottom: 6px;
|
margin-bottom: 6px;
|
||||||
font-size: 1.25rem;
|
font-size: 1.25rem;
|
||||||
color: #495057;
|
color: var(--input-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-tagsinput-input::placeholder {
|
||||||
|
color: $textPlaceholder;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-autosuggest__suggestion--highlighted {
|
||||||
|
background-color: var(--active-color);
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,7 +43,7 @@ bottle.serviceFactory(
|
||||||
'Settings',
|
'Settings',
|
||||||
'ShlinkVersionsContainer',
|
'ShlinkVersionsContainer',
|
||||||
);
|
);
|
||||||
bottle.decorator('App', connect([ 'servers' ], [ 'fetchServers' ]));
|
bottle.decorator('App', connect([ 'servers', 'settings' ], [ 'fetchServers' ]));
|
||||||
|
|
||||||
provideCommonServices(bottle, connect, withRouter);
|
provideCommonServices(bottle, connect, withRouter);
|
||||||
provideApiServices(bottle);
|
provideApiServices(bottle);
|
||||||
|
|
|
@ -1,12 +1,19 @@
|
||||||
|
@import '../utils/base';
|
||||||
@import '../utils/mixins/vertical-align';
|
@import '../utils/mixins/vertical-align';
|
||||||
|
|
||||||
|
.domains-dropdown__toggle-btn.domains-dropdown__toggle-btn,
|
||||||
|
.domains-dropdown__toggle-btn.domains-dropdown__toggle-btn:hover,
|
||||||
|
.domains-dropdown__toggle-btn.domains-dropdown__toggle-btn:active {
|
||||||
|
color: $textPlaceholder !important;
|
||||||
|
}
|
||||||
|
|
||||||
.domains-dropdown__toggle-btn--active.domains-dropdown__toggle-btn--active,
|
.domains-dropdown__toggle-btn--active.domains-dropdown__toggle-btn--active,
|
||||||
.domains-dropdown__toggle-btn--active.domains-dropdown__toggle-btn--active:hover,
|
.domains-dropdown__toggle-btn--active.domains-dropdown__toggle-btn--active:hover,
|
||||||
.domains-dropdown__toggle-btn--active.domains-dropdown__toggle-btn--active:active {
|
.domains-dropdown__toggle-btn--active.domains-dropdown__toggle-btn--active:active {
|
||||||
color: #495057 !important;
|
color: var(--input-text-color) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.domains-dropdown__back-btn.domains-dropdown__back-btn,
|
.domains-dropdown__back-btn.domains-dropdown__back-btn,
|
||||||
.domains-dropdown__back-btn.domains-dropdown__back-btn:hover {
|
.domains-dropdown__back-btn.domains-dropdown__back-btn:hover {
|
||||||
border-color: #ced4da;
|
border-color: var(--border-color);
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,7 +54,7 @@ export const DomainSelector = ({ listDomains, value, domainsList, onChange }: Do
|
||||||
) : (
|
) : (
|
||||||
<DropdownBtn
|
<DropdownBtn
|
||||||
text={valueIsEmpty ? 'Domain' : `Domain: ${value}`}
|
text={valueIsEmpty ? 'Domain' : `Domain: ${value}`}
|
||||||
className={!valueIsEmpty ? 'domains-dropdown__toggle-btn--active' : ''}
|
className={!valueIsEmpty ? 'domains-dropdown__toggle-btn--active' : 'domains-dropdown__toggle-btn'}
|
||||||
>
|
>
|
||||||
{domains.map(({ domain, isDefault }) => (
|
{domains.map(({ domain, isDefault }) => (
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
|
|
138
src/index.scss
138
src/index.scss
|
@ -1,32 +1,87 @@
|
||||||
|
/* stylelint-disable no-descending-specificity */
|
||||||
|
|
||||||
@import './utils/base';
|
@import './utils/base';
|
||||||
@import 'node_modules/bootstrap/scss/bootstrap.scss';
|
@import 'node_modules/bootstrap/scss/bootstrap.scss';
|
||||||
@import './common/react-tagsinput.scss';
|
@import './common/react-tagsinput.scss';
|
||||||
|
@import './theme/theme';
|
||||||
|
|
||||||
|
* {
|
||||||
|
outline: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
html,
|
html,
|
||||||
body,
|
body,
|
||||||
#root {
|
#root {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: $lightColor;
|
background: var(--secondary-color);
|
||||||
}
|
color: var(--text-color);
|
||||||
|
|
||||||
* {
|
|
||||||
outline: none !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.bg-main {
|
.bg-main {
|
||||||
background-color: $mainColor !important;
|
background-color: $mainColor !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card-body,
|
||||||
box-shadow: 0 .125rem .25rem rgba(0, 0, 0, .075);
|
.card-header,
|
||||||
}
|
.list-group-item {
|
||||||
|
background-color: transparent;
|
||||||
.card-header {
|
|
||||||
background-color: white;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-footer {
|
.card-footer {
|
||||||
background-color: rgba(255, 255, 255, .5);
|
background-color: var(--primary-color-alfa);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
box-shadow: 0 .125rem .25rem rgba(0, 0, 0, .075);
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
border-color: var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content,
|
||||||
|
.page-link,
|
||||||
|
.page-item.disabled .page-link,
|
||||||
|
.dropdown-menu {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header,
|
||||||
|
.modal-footer,
|
||||||
|
.card-header,
|
||||||
|
.card-footer,
|
||||||
|
.table thead th,
|
||||||
|
.table th,
|
||||||
|
.table td,
|
||||||
|
.page-link,
|
||||||
|
.page-link:hover,
|
||||||
|
.page-item.disabled .page-link,
|
||||||
|
.dropdown-divider,
|
||||||
|
.dropdown-menu,
|
||||||
|
.list-group-item,
|
||||||
|
.modal-content {
|
||||||
|
border-color: var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-bordered,
|
||||||
|
.table-bordered thead th,
|
||||||
|
.table-bordered thead td {
|
||||||
|
border-color: var(--table-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-link:hover {
|
||||||
|
background-color: var(--secondary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-item.active .page-link {
|
||||||
|
background-color: var(--brand-color);
|
||||||
|
border-color: var(--brand-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination .page-link {
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container-xl {
|
.container-xl {
|
||||||
|
@ -40,32 +95,61 @@ body,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dropdown-item,
|
||||||
|
.dropdown-item-text {
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
.dropdown-item:not(:disabled) {
|
.dropdown-item:not(:disabled) {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dropdown-item:focus:not(:disabled),
|
||||||
|
.dropdown-item:hover:not(:disabled),
|
||||||
.dropdown-item.active:not(:disabled),
|
.dropdown-item.active:not(:disabled),
|
||||||
.dropdown-item:active:not(:disabled) {
|
.dropdown-item:active:not(:disabled) {
|
||||||
background-color: $lightGrey !important;
|
background-color: var(--active-color) !important;
|
||||||
color: inherit !important;
|
color: var(--text-color) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge-main {
|
.badge-main {
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
background-color: $mainColor;
|
background-color: var(--brand-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.close,
|
||||||
|
.close:hover,
|
||||||
|
.table,
|
||||||
|
.table-hover tbody tr:hover {
|
||||||
|
color: var(--text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-hover tbody tr:hover {
|
.table-hover tbody tr:hover {
|
||||||
background-color: $lightColor;
|
background-color: var(--secondary-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-datepicker__input-container,
|
.form-control,
|
||||||
.react-datepicker-wrapper {
|
.form-control:focus {
|
||||||
display: block !important;
|
background-color: var(--primary-color);
|
||||||
|
border-color: var(--input-border-color);
|
||||||
|
color: var(--input-text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-datepicker-popper {
|
.form-control.disabled,
|
||||||
z-index: 2;
|
.form-control:disabled {
|
||||||
|
background-color: var(--input-disabled-color);
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card .form-control:not(:disabled),
|
||||||
|
.card .form-control:not(:disabled):hover {
|
||||||
|
background-color: var(--input-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-active,
|
||||||
|
.table-active > th,
|
||||||
|
.table-active > td {
|
||||||
|
background-color: var(--table-highlight-color) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-brand {
|
.navbar-brand {
|
||||||
|
@ -74,10 +158,6 @@ body,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination .page-link {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.indivisible {
|
.indivisible {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
@ -92,14 +172,6 @@ body,
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-datepicker__day--keyboard-selected {
|
|
||||||
background-color: $mainColor;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: darken($mainColor, 12%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-bar {
|
.progress-bar {
|
||||||
background-color: $mainColor;
|
background-color: $mainColor;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,10 +2,10 @@
|
||||||
|
|
||||||
.overview__card.overview__card {
|
.overview__card.overview__card {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
border-top: 3px solid $mainColor;
|
border-top: 3px solid var(--brand-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.overview__card-title {
|
.overview__card-title {
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
color: #6c757d;
|
color: $textPlaceholder;
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.servers-list__server-item:hover {
|
.servers-list__server-item:hover {
|
||||||
background-color: $lightColor;
|
background-color: var(--secondary-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.servers-list__server-item-icon {
|
.servers-list__server-item-icon {
|
||||||
|
@ -29,7 +29,7 @@
|
||||||
|
|
||||||
.servers-list__list-group--embedded.servers-list__list-group--embedded {
|
.servers-list__list-group--embedded.servers-list__list-group--embedded {
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
border-top: 1px solid rgba(0, 0, 0, .125);
|
border-top: 1px solid var(--border-color);
|
||||||
|
|
||||||
@media (min-width: $mdMin) {
|
@media (min-width: $mdMin) {
|
||||||
max-height: 220px;
|
max-height: 220px;
|
||||||
|
@ -40,6 +40,6 @@
|
||||||
|
|
||||||
.servers-list__server-item {
|
.servers-list__server-item {
|
||||||
border: none;
|
border: none;
|
||||||
border-bottom: 1px solid rgba(0, 0, 0, .125);
|
border-bottom: 1px solid var(--border-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,9 @@ const RealTimeUpdates = (
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<ToggleSwitch checked={realTimeUpdates.enabled} onChange={toggleRealTimeUpdates}>
|
<ToggleSwitch checked={realTimeUpdates.enabled} onChange={toggleRealTimeUpdates}>
|
||||||
Enable or disable real-time updates, when using Shlink v2.2.0 or newer.
|
Enable or disable real-time updates, when using Shlink v2.2.0 or newer.
|
||||||
|
<small className="form-text text-muted">
|
||||||
|
Real-time updates are currently being <b>{realTimeUpdates.enabled ? 'processed' : 'ignored'}</b>.
|
||||||
|
</small>
|
||||||
</ToggleSwitch>
|
</ToggleSwitch>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
<FormGroup className="mb-0">
|
<FormGroup className="mb-0">
|
||||||
|
|
|
@ -2,14 +2,19 @@ import { FC } from 'react';
|
||||||
import { Row } from 'reactstrap';
|
import { Row } from 'reactstrap';
|
||||||
import NoMenuLayout from '../common/NoMenuLayout';
|
import NoMenuLayout from '../common/NoMenuLayout';
|
||||||
|
|
||||||
const Settings = (RealTimeUpdates: FC, ShortUrlCreation: FC) => () => (
|
const Settings = (RealTimeUpdates: FC, ShortUrlCreation: FC, UserInterface: FC) => () => (
|
||||||
<NoMenuLayout>
|
<NoMenuLayout>
|
||||||
<Row>
|
<Row>
|
||||||
<div className="col-lg-6">
|
<div className="col-lg-6">
|
||||||
<RealTimeUpdates />
|
<div className="mb-3 mb-md-4">
|
||||||
|
<UserInterface />
|
||||||
|
</div>
|
||||||
|
<div className="mb-3 mb-md-4">
|
||||||
|
<ShortUrlCreation />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-lg-6">
|
<div className="col-lg-6">
|
||||||
<ShortUrlCreation />
|
<RealTimeUpdates />
|
||||||
</div>
|
</div>
|
||||||
</Row>
|
</Row>
|
||||||
</NoMenuLayout>
|
</NoMenuLayout>
|
||||||
|
|
4
src/settings/UserInterface.scss
Normal file
4
src/settings/UserInterface.scss
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
.user-interface__theme-icon {
|
||||||
|
float: right;
|
||||||
|
margin-top: .25rem;
|
||||||
|
}
|
30
src/settings/UserInterface.tsx
Normal file
30
src/settings/UserInterface.tsx
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import { FC } from 'react';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import { faSun, faMoon } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { SimpleCard } from '../utils/SimpleCard';
|
||||||
|
import ToggleSwitch from '../utils/ToggleSwitch';
|
||||||
|
import { changeThemeInMarkup, Theme } from '../utils/theme';
|
||||||
|
import { Settings, UiSettings } from './reducers/settings';
|
||||||
|
import './UserInterface.scss';
|
||||||
|
|
||||||
|
interface UserInterfaceProps {
|
||||||
|
settings: Settings;
|
||||||
|
setUiSettings: (settings: UiSettings) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UserInterface: FC<UserInterfaceProps> = ({ settings: { ui }, setUiSettings }) => (
|
||||||
|
<SimpleCard title="User interface">
|
||||||
|
<FontAwesomeIcon icon={ui?.theme === 'dark' ? faMoon : faSun} className="user-interface__theme-icon" />
|
||||||
|
<ToggleSwitch
|
||||||
|
checked={ui?.theme === 'dark'}
|
||||||
|
onChange={(useDarkTheme) => {
|
||||||
|
const theme: Theme = useDarkTheme ? 'dark' : 'light';
|
||||||
|
|
||||||
|
setUiSettings({ theme });
|
||||||
|
changeThemeInMarkup(theme);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Use dark theme.
|
||||||
|
</ToggleSwitch>
|
||||||
|
</SimpleCard>
|
||||||
|
);
|
|
@ -2,6 +2,7 @@ import { Action } from 'redux';
|
||||||
import { dissoc, mergeDeepRight } from 'ramda';
|
import { dissoc, mergeDeepRight } from 'ramda';
|
||||||
import { buildReducer } from '../../utils/helpers/redux';
|
import { buildReducer } from '../../utils/helpers/redux';
|
||||||
import { RecursivePartial } from '../../utils/utils';
|
import { RecursivePartial } from '../../utils/utils';
|
||||||
|
import { Theme } from '../../utils/theme';
|
||||||
|
|
||||||
export const SET_SETTINGS = 'shlink/realTimeUpdates/SET_SETTINGS';
|
export const SET_SETTINGS = 'shlink/realTimeUpdates/SET_SETTINGS';
|
||||||
|
|
||||||
|
@ -19,9 +20,14 @@ export interface ShortUrlCreationSettings {
|
||||||
validateUrls: boolean;
|
validateUrls: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UiSettings {
|
||||||
|
theme: Theme;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Settings {
|
export interface Settings {
|
||||||
realTimeUpdates: RealTimeUpdatesSettings;
|
realTimeUpdates: RealTimeUpdatesSettings;
|
||||||
shortUrlCreation?: ShortUrlCreationSettings;
|
shortUrlCreation?: ShortUrlCreationSettings;
|
||||||
|
ui?: UiSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: Settings = {
|
const initialState: Settings = {
|
||||||
|
@ -31,6 +37,9 @@ const initialState: Settings = {
|
||||||
shortUrlCreation: {
|
shortUrlCreation: {
|
||||||
validateUrls: false,
|
validateUrls: false,
|
||||||
},
|
},
|
||||||
|
ui: {
|
||||||
|
theme: 'light',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
type SettingsAction = Action & Settings;
|
type SettingsAction = Action & Settings;
|
||||||
|
@ -55,3 +64,8 @@ export const setShortUrlCreationSettings = (settings: ShortUrlCreationSettings):
|
||||||
type: SET_SETTINGS,
|
type: SET_SETTINGS,
|
||||||
shortUrlCreation: settings,
|
shortUrlCreation: settings,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const setUiSettings = (settings: UiSettings): PartialSettingsAction => ({
|
||||||
|
type: SET_SETTINGS,
|
||||||
|
ui: settings,
|
||||||
|
});
|
||||||
|
|
|
@ -1,14 +1,20 @@
|
||||||
import Bottle from 'bottlejs';
|
import Bottle from 'bottlejs';
|
||||||
import RealTimeUpdates from '../RealTimeUpdates';
|
import RealTimeUpdates from '../RealTimeUpdates';
|
||||||
import Settings from '../Settings';
|
import Settings from '../Settings';
|
||||||
import { setRealTimeUpdatesInterval, setShortUrlCreationSettings, toggleRealTimeUpdates } from '../reducers/settings';
|
import {
|
||||||
|
setRealTimeUpdatesInterval,
|
||||||
|
setShortUrlCreationSettings,
|
||||||
|
setUiSettings,
|
||||||
|
toggleRealTimeUpdates,
|
||||||
|
} from '../reducers/settings';
|
||||||
import { ConnectDecorator } from '../../container/types';
|
import { ConnectDecorator } from '../../container/types';
|
||||||
import { withoutSelectedServer } from '../../servers/helpers/withoutSelectedServer';
|
import { withoutSelectedServer } from '../../servers/helpers/withoutSelectedServer';
|
||||||
import { ShortUrlCreation } from '../ShortUrlCreation';
|
import { ShortUrlCreation } from '../ShortUrlCreation';
|
||||||
|
import { UserInterface } from '../UserInterface';
|
||||||
|
|
||||||
const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
||||||
// Components
|
// Components
|
||||||
bottle.serviceFactory('Settings', Settings, 'RealTimeUpdates', 'ShortUrlCreation');
|
bottle.serviceFactory('Settings', Settings, 'RealTimeUpdates', 'ShortUrlCreation', 'UserInterface');
|
||||||
bottle.decorator('Settings', withoutSelectedServer);
|
bottle.decorator('Settings', withoutSelectedServer);
|
||||||
bottle.decorator('Settings', connect(null, [ 'resetSelectedServer' ]));
|
bottle.decorator('Settings', connect(null, [ 'resetSelectedServer' ]));
|
||||||
|
|
||||||
|
@ -21,10 +27,14 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
||||||
bottle.serviceFactory('ShortUrlCreation', () => ShortUrlCreation);
|
bottle.serviceFactory('ShortUrlCreation', () => ShortUrlCreation);
|
||||||
bottle.decorator('ShortUrlCreation', connect([ 'settings' ], [ 'setShortUrlCreationSettings' ]));
|
bottle.decorator('ShortUrlCreation', connect([ 'settings' ], [ 'setShortUrlCreationSettings' ]));
|
||||||
|
|
||||||
|
bottle.serviceFactory('UserInterface', () => UserInterface);
|
||||||
|
bottle.decorator('UserInterface', connect([ 'settings' ], [ 'setUiSettings' ]));
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
bottle.serviceFactory('toggleRealTimeUpdates', () => toggleRealTimeUpdates);
|
bottle.serviceFactory('toggleRealTimeUpdates', () => toggleRealTimeUpdates);
|
||||||
bottle.serviceFactory('setRealTimeUpdatesInterval', () => setRealTimeUpdatesInterval);
|
bottle.serviceFactory('setRealTimeUpdatesInterval', () => setRealTimeUpdatesInterval);
|
||||||
bottle.serviceFactory('setShortUrlCreationSettings', () => setShortUrlCreationSettings);
|
bottle.serviceFactory('setShortUrlCreationSettings', () => setShortUrlCreationSettings);
|
||||||
|
bottle.serviceFactory('setUiSettings', () => setUiSettings);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default provideServices;
|
export default provideServices;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
.short-urls-paginator {
|
.short-urls-paginator {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
background-color: rgba(255, 255, 255, .5);
|
background-color: var(--primary-color-alfa);
|
||||||
padding: .75rem 0;
|
padding: .75rem 0;
|
||||||
border-top: 1px solid rgba(black, .125);
|
border-top: 1px solid var(--border-color);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { faInfoCircle as infoIcon } from '@fortawesome/free-solid-svg-icons';
|
import { faInfoCircle as infoIcon } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { Modal, ModalBody, ModalHeader } from 'reactstrap';
|
import { Modal, ModalBody, ModalHeader } from 'reactstrap';
|
||||||
import './UseExistingIfFoundInfoIcon.scss';
|
|
||||||
import { useToggle } from '../utils/helpers/hooks';
|
import { useToggle } from '../utils/helpers/hooks';
|
||||||
|
import './UseExistingIfFoundInfoIcon.scss';
|
||||||
|
|
||||||
const InfoModal = ({ isOpen, toggle }: { isOpen: boolean; toggle: () => void }) => (
|
const InfoModal = ({ isOpen, toggle }: { isOpen: boolean; toggle: () => void }) => (
|
||||||
<Modal isOpen={isOpen} toggle={toggle} centered size="lg">
|
<Modal isOpen={isOpen} toggle={toggle} centered size="lg">
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { DropdownItem, FormGroup, Modal, ModalBody, ModalHeader, Row } from 'reactstrap';
|
import { Modal, DropdownItem, FormGroup, ModalBody, ModalHeader, Row } from 'reactstrap';
|
||||||
import { ExternalLink } from 'react-external-link';
|
import { ExternalLink } from 'react-external-link';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { ShortUrlModalProps } from '../data';
|
import { ShortUrlModalProps } from '../data';
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
@media (max-width: $responsiveTableBreakpoint) {
|
@media (max-width: $responsiveTableBreakpoint) {
|
||||||
display: block;
|
display: block;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
border-bottom: 1px solid $lightGrey;
|
border-bottom: 1px solid var(--border-color);
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
@import '../utils/base';
|
||||||
|
|
||||||
.tag-card.tag-card {
|
.tag-card.tag-card {
|
||||||
margin-bottom: .5rem;
|
margin-bottom: .5rem;
|
||||||
}
|
}
|
||||||
|
@ -26,11 +28,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag-card__tag-name {
|
.tag-card__tag-name {
|
||||||
color: #007bff;
|
color: $mainColor;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag-card__tag-name:hover {
|
.tag-card__tag-name:hover {
|
||||||
color: #0056b3;
|
color: darken($mainColor, 15%);
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,10 +35,10 @@ const TagCard = (
|
||||||
return (
|
return (
|
||||||
<Card className="tag-card">
|
<Card className="tag-card">
|
||||||
<CardHeader className="tag-card__header">
|
<CardHeader className="tag-card__header">
|
||||||
<Button color="light" size="sm" className="tag-card__btn tag-card__btn--last" onClick={toggleDelete}>
|
<Button color="link" size="sm" className="tag-card__btn tag-card__btn--last" onClick={toggleDelete}>
|
||||||
<FontAwesomeIcon icon={deleteIcon} />
|
<FontAwesomeIcon icon={deleteIcon} />
|
||||||
</Button>
|
</Button>
|
||||||
<Button color="light" size="sm" className="tag-card__btn" onClick={toggleEdit}>
|
<Button color="link" size="sm" className="tag-card__btn" onClick={toggleEdit}>
|
||||||
<FontAwesomeIcon icon={editIcon} />
|
<FontAwesomeIcon icon={editIcon} />
|
||||||
</Button>
|
</Button>
|
||||||
<h5 className="tag-card__tag-title text-ellipsis">
|
<h5 className="tag-card__tag-title text-ellipsis">
|
||||||
|
@ -57,14 +57,14 @@ const TagCard = (
|
||||||
<CardBody className="tag-card__body">
|
<CardBody className="tag-card__body">
|
||||||
<Link
|
<Link
|
||||||
to={shortUrlsLink}
|
to={shortUrlsLink}
|
||||||
className="btn btn-light btn-block d-flex justify-content-between align-items-center mb-1"
|
className="btn btn-outline-secondary btn-block d-flex justify-content-between align-items-center mb-1"
|
||||||
>
|
>
|
||||||
<span className="text-ellipsis"><FontAwesomeIcon icon={faLink} className="mr-2" />Short URLs</span>
|
<span className="text-ellipsis"><FontAwesomeIcon icon={faLink} className="mr-2" />Short URLs</span>
|
||||||
<b>{prettify(tagStats.shortUrlsCount)}</b>
|
<b>{prettify(tagStats.shortUrlsCount)}</b>
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
to={`/server/${serverId}/tag/${tag}/visits`}
|
to={`/server/${serverId}/tag/${tag}/visits`}
|
||||||
className="btn btn-light btn-block d-flex justify-content-between align-items-center"
|
className="btn btn-outline-secondary btn-block d-flex justify-content-between align-items-center"
|
||||||
>
|
>
|
||||||
<span className="text-ellipsis"><FontAwesomeIcon icon={faEye} className="mr-2" />Visits</span>
|
<span className="text-ellipsis"><FontAwesomeIcon icon={faEye} className="mr-2" />Visits</span>
|
||||||
<b>{prettify(tagStats.visitsCount)}</b>
|
<b>{prettify(tagStats.visitsCount)}</b>
|
||||||
|
|
63
src/theme/theme.scss
Normal file
63
src/theme/theme.scss
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
@import '../utils/base';
|
||||||
|
|
||||||
|
// Light theme colors
|
||||||
|
$lightPrimaryColor: #ffffff;
|
||||||
|
$lightPrimaryColorAlfa: rgba($lightPrimaryColor, .5);
|
||||||
|
$lightSecondaryColor: $lightColor;
|
||||||
|
$lightTextColor: #212529;
|
||||||
|
$lightBorderColor: rgba(0, 0, 0, .125);
|
||||||
|
$lightTableBorderColor: $mediumGrey;
|
||||||
|
$lightActiveColor: $lightGrey;
|
||||||
|
$lightBrandColor: $mainColor;
|
||||||
|
$lightInputColor: $lightPrimaryColor;
|
||||||
|
$lightInputTextColor: #495057;
|
||||||
|
$lightDisabledInputColor: $lightColor;
|
||||||
|
$lightBorderInputColor: rgba(0, 0, 0, .19);
|
||||||
|
$lightTableHighlightColor: rgba(0, 0, 0, .075);
|
||||||
|
|
||||||
|
// Dark theme colors
|
||||||
|
$darkPrimaryColor: #161b22;
|
||||||
|
$darkPrimaryColorAlfa: rgba($darkPrimaryColor, .8);
|
||||||
|
$darkSecondaryColor: #0f131a;
|
||||||
|
$darkTextColor: rgb(201, 209, 217);
|
||||||
|
$darkBorderColor: rgba(255, 255, 255, .15);
|
||||||
|
$darkTableBorderColor: #393d43;
|
||||||
|
$darkActiveColor: $darkSecondaryColor;
|
||||||
|
$darkBrandColor: #0b2d4e;
|
||||||
|
$darkInputColor: darken($darkPrimaryColor, 2%);
|
||||||
|
$darkInputTextColor: $darkTextColor;
|
||||||
|
$darkDisabledInputColor: lighten($darkPrimaryColor, 2%);
|
||||||
|
$darkBorderInputColor: $darkBorderColor;
|
||||||
|
$darkTableHighlightColor: $darkBorderColor;
|
||||||
|
|
||||||
|
html:not([data-theme='dark']) {
|
||||||
|
--primary-color: #{$lightPrimaryColor};
|
||||||
|
--primary-color-alfa: #{$lightPrimaryColorAlfa};
|
||||||
|
--secondary-color: #{$lightSecondaryColor};
|
||||||
|
--text-color: #{$lightTextColor};
|
||||||
|
--border-color: #{$lightBorderColor};
|
||||||
|
--active-color: #{$lightActiveColor};
|
||||||
|
--brand-color: #{$lightBrandColor};
|
||||||
|
--input-color: #{$lightInputColor};
|
||||||
|
--input-disabled-color: #{$lightDisabledInputColor};
|
||||||
|
--input-border-color: #{$lightBorderInputColor};
|
||||||
|
--input-text-color: #{$lightInputTextColor};
|
||||||
|
--table-border-color: #{$lightTableBorderColor};
|
||||||
|
--table-highlight-color: #{$lightTableHighlightColor};
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme='dark'] {
|
||||||
|
--primary-color: #{$darkPrimaryColor};
|
||||||
|
--primary-color-alfa: #{$darkPrimaryColorAlfa};
|
||||||
|
--secondary-color: #{$darkSecondaryColor};
|
||||||
|
--text-color: #{$darkTextColor};
|
||||||
|
--border-color: #{$darkBorderColor};
|
||||||
|
--active-color: #{$darkActiveColor};
|
||||||
|
--brand-color: #{$darkBrandColor};
|
||||||
|
--input-color: #{$darkInputColor};
|
||||||
|
--input-disabled-color: #{$darkDisabledInputColor};
|
||||||
|
--input-border-color: #{$darkBorderInputColor};
|
||||||
|
--input-text-color: #{$darkInputTextColor};
|
||||||
|
--table-border-color: #{$darkTableBorderColor};
|
||||||
|
--table-highlight-color: #{$darkTableHighlightColor};
|
||||||
|
}
|
|
@ -10,7 +10,12 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.date-input-container__input:not(:disabled) {
|
.date-input-container__input:not(:disabled) {
|
||||||
background-color: #ffffff !important;
|
background-color: var(--primary-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card .date-input-container__input:not(:disabled),
|
||||||
|
.dropdown .date-input-container__input:not(:disabled) {
|
||||||
|
background-color: var(--input-color) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.date-input-container__icon {
|
.date-input-container__icon {
|
||||||
|
@ -32,3 +37,66 @@
|
||||||
background-color: #333333;
|
background-color: #333333;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.react-datepicker__input-container,
|
||||||
|
.react-datepicker-wrapper {
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-datepicker__day--keyboard-selected {
|
||||||
|
background-color: $mainColor;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: darken($mainColor, 12%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-datepicker.react-datepicker {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
border-color: var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-datepicker__header.react-datepicker__header {
|
||||||
|
background-color: var(--secondary-color);
|
||||||
|
border-color: var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-datepicker__current-month.react-datepicker__current-month,
|
||||||
|
.react-datepicker-time__header.react-datepicker-time__header,
|
||||||
|
.react-datepicker-year-header.react-datepicker-year-header,
|
||||||
|
.react-datepicker__day-name.react-datepicker__day-name,
|
||||||
|
.react-datepicker__day:not(:hover).react-datepicker__day:not(:hover),
|
||||||
|
.react-datepicker__time-name.react-datepicker__time-name {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-datepicker__day--keyboard-selected.react-datepicker__day--keyboard-selected,
|
||||||
|
.react-datepicker__month-text--keyboard-selected.react-datepicker__month-text--keyboard-selected,
|
||||||
|
.react-datepicker__quarter-text--keyboard-selected.react-datepicker__quarter-text--keyboard-selected,
|
||||||
|
.react-datepicker__year-text--keyboard-selected.react-datepicker__year-text--keyboard-selected {
|
||||||
|
background-color: var(--brand-color) !important;
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-datepicker-popper.react-datepicker-popper {
|
||||||
|
z-index: 2;
|
||||||
|
|
||||||
|
&[data-placement^='top'] .react-datepicker__triangle.react-datepicker__triangle {
|
||||||
|
border-top-color: var(--primary-color);
|
||||||
|
border-bottom-color: var(--border-color);
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
border-top-color: var(--border-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-placement^='bottom'] .react-datepicker__triangle.react-datepicker__triangle {
|
||||||
|
border-top-color: var(--border-color);
|
||||||
|
border-bottom-color: var(--secondary-color);
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
border-bottom-color: var(--border-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
/* stylelint-disable no-descending-specificity */
|
||||||
|
|
||||||
@import '../utils/mixins/vertical-align';
|
@import '../utils/mixins/vertical-align';
|
||||||
|
|
||||||
.dropdown-btn__toggle.dropdown-btn__toggle,
|
.dropdown-btn__toggle.dropdown-btn__toggle,
|
||||||
|
@ -6,10 +8,24 @@
|
||||||
.dropdown-btn__toggle.dropdown-btn__toggle:not(:disabled):not(.disabled):focus,
|
.dropdown-btn__toggle.dropdown-btn__toggle:not(:disabled):not(.disabled):focus,
|
||||||
.dropdown-btn__toggle.dropdown-btn__toggle:not(:disabled):not(.disabled):hover,
|
.dropdown-btn__toggle.dropdown-btn__toggle:not(:disabled):not(.disabled):hover,
|
||||||
.show > .dropdown-btn__toggle.dropdown-btn__toggle.dropdown-toggle {
|
.show > .dropdown-btn__toggle.dropdown-btn__toggle.dropdown-toggle {
|
||||||
color: #6c757d;
|
|
||||||
background-color: white;
|
|
||||||
text-align: left;
|
text-align: left;
|
||||||
border-color: rgba(0, 0, 0, .125);
|
color: var(--input-text-color);
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
border-color: var(--input-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card .dropdown-btn__toggle.dropdown-btn__toggle,
|
||||||
|
.card .dropdown-btn__toggle.dropdown-btn__toggle:not(:disabled):not(.disabled).active,
|
||||||
|
.card .dropdown-btn__toggle.dropdown-btn__toggle:not(:disabled):not(.disabled):active,
|
||||||
|
.card .dropdown-btn__toggle.dropdown-btn__toggle:not(:disabled):not(.disabled):focus,
|
||||||
|
.card .dropdown-btn__toggle.dropdown-btn__toggle:not(:disabled):not(.disabled):hover,
|
||||||
|
.show > .card .dropdown-btn__toggle.dropdown-btn__toggle.dropdown-toggle {
|
||||||
|
background-color: var(--input-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-btn__toggle.dropdown-btn__toggle.disabled,
|
||||||
|
.dropdown-btn__toggle.dropdown-btn__toggle:disabled {
|
||||||
|
background-color: var(--input-disabled-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-btn__toggle.dropdown-btn__toggle:after {
|
.dropdown-btn__toggle.dropdown-btn__toggle:after {
|
||||||
|
|
|
@ -12,9 +12,10 @@ $responsiveTableBreakpoint: $mdMax;
|
||||||
// Colors
|
// Colors
|
||||||
$mainColor: #4696e5;
|
$mainColor: #4696e5;
|
||||||
$lightColor: #f5f6fe;
|
$lightColor: #f5f6fe;
|
||||||
$lightGrey: #dddddd;
|
$lightGrey: #eeeeee;
|
||||||
$dangerColor: #dc3545;
|
$dangerColor: #dc3545;
|
||||||
$mediumGrey: #dee2e6;
|
$mediumGrey: #dee2e6;
|
||||||
|
$textPlaceholder: #6c757d;
|
||||||
|
|
||||||
// Misc
|
// Misc
|
||||||
$headerHeight: 57px;
|
$headerHeight: 57px;
|
||||||
|
@ -23,6 +24,6 @@ $footer-height: 2.3rem;
|
||||||
$footer-margin: .8rem;
|
$footer-margin: .8rem;
|
||||||
|
|
||||||
// Bootstrap overwrites
|
// Bootstrap overwrites
|
||||||
//$theme-colors: (
|
$theme-colors: (
|
||||||
// 'primary': $mainColor
|
'primary': $mainColor
|
||||||
//);
|
);
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
left: 0;
|
left: 0;
|
||||||
bottom: -1px;
|
bottom: -1px;
|
||||||
right: -1px;
|
right: -1px;
|
||||||
background: $mediumGrey;
|
background: var(--table-border-color);
|
||||||
z-index: -2;
|
z-index: -2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@
|
||||||
left: 1px;
|
left: 1px;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
background: white;
|
background: var(--primary-color);
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,24 @@ export const MAIN_COLOR = '#4696e5';
|
||||||
|
|
||||||
export const MAIN_COLOR_ALPHA = 'rgba(70, 150, 229, 0.4)';
|
export const MAIN_COLOR_ALPHA = 'rgba(70, 150, 229, 0.4)';
|
||||||
|
|
||||||
export const HIGHLIGHTED_COLOR = '#F77F28';
|
export const HIGHLIGHTED_COLOR = '#f77f28';
|
||||||
|
|
||||||
export const HIGHLIGHTED_COLOR_ALPHA = 'rgba(247, 127, 40, 0.4)';
|
export const HIGHLIGHTED_COLOR_ALPHA = 'rgba(247, 127, 40, 0.4)';
|
||||||
|
|
||||||
|
export const PRIMARY_LIGHT_COLOR = 'white';
|
||||||
|
|
||||||
|
export const PRIMARY_DARK_COLOR = '#161b22';
|
||||||
|
|
||||||
|
export type Theme = 'dark' | 'light';
|
||||||
|
|
||||||
|
export const changeThemeInMarkup = (theme: Theme) => {
|
||||||
|
const html = document.getElementsByTagName('html');
|
||||||
|
|
||||||
|
html?.[0]?.setAttribute('data-theme', theme);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isDarkThemeEnabled = (): boolean => {
|
||||||
|
const html = document.getElementsByTagName('html');
|
||||||
|
|
||||||
|
return html?.[0]?.getAttribute('data-theme') === 'dark';
|
||||||
|
};
|
||||||
|
|
|
@ -25,6 +25,6 @@
|
||||||
|
|
||||||
.visits-stats__nav-link.active {
|
.visits-stats__nav-link.active {
|
||||||
border-color: $mainColor;
|
border-color: $mainColor;
|
||||||
background-color: white !important;
|
background-color: var(--primary-color) !important;
|
||||||
color: $mainColor !important;
|
color: $mainColor !important;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
.visits-table {
|
.visits-table {
|
||||||
margin: 1.5rem 0 0;
|
margin: 1.5rem 0 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
background-color: white;
|
background-color: var(--primary-color);
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -154,7 +154,7 @@ const VisitsTable = ({
|
||||||
<tr
|
<tr
|
||||||
key={index}
|
key={index}
|
||||||
style={{ cursor: 'pointer' }}
|
style={{ cursor: 'pointer' }}
|
||||||
className={classNames({ 'table-primary': isSelected })}
|
className={classNames({ 'table-active': isSelected })}
|
||||||
onClick={() => setSelectedVisits(
|
onClick={() => setSelectedVisits(
|
||||||
isSelected ? selectedVisits.filter((v) => v !== visit) : [ ...selectedVisits, visit ],
|
isSelected ? selectedVisits.filter((v) => v !== visit) : [ ...selectedVisits, visit ],
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -7,7 +7,15 @@ import { fillTheGaps } from '../../utils/helpers/visits';
|
||||||
import { Stats } from '../types';
|
import { Stats } from '../types';
|
||||||
import { prettify } from '../../utils/helpers/numbers';
|
import { prettify } from '../../utils/helpers/numbers';
|
||||||
import { pointerOnHover, renderDoughnutChartLabel, renderNonDoughnutChartLabel } from '../../utils/helpers/charts';
|
import { pointerOnHover, renderDoughnutChartLabel, renderNonDoughnutChartLabel } from '../../utils/helpers/charts';
|
||||||
import { HIGHLIGHTED_COLOR, HIGHLIGHTED_COLOR_ALPHA, MAIN_COLOR, MAIN_COLOR_ALPHA } from '../../utils/theme';
|
import {
|
||||||
|
HIGHLIGHTED_COLOR,
|
||||||
|
HIGHLIGHTED_COLOR_ALPHA,
|
||||||
|
isDarkThemeEnabled,
|
||||||
|
MAIN_COLOR,
|
||||||
|
MAIN_COLOR_ALPHA,
|
||||||
|
PRIMARY_DARK_COLOR,
|
||||||
|
PRIMARY_LIGHT_COLOR,
|
||||||
|
} from '../../utils/theme';
|
||||||
import './DefaultChart.scss';
|
import './DefaultChart.scss';
|
||||||
|
|
||||||
export interface DefaultChartProps {
|
export interface DefaultChartProps {
|
||||||
|
@ -47,7 +55,7 @@ const generateGraphData = (
|
||||||
'#DCDCDC',
|
'#DCDCDC',
|
||||||
'#463730',
|
'#463730',
|
||||||
],
|
],
|
||||||
borderColor: isBarChart ? MAIN_COLOR : 'white',
|
borderColor: isBarChart ? MAIN_COLOR : (isDarkThemeEnabled() ? PRIMARY_DARK_COLOR : PRIMARY_LIGHT_COLOR),
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
},
|
},
|
||||||
highlightedData && {
|
highlightedData && {
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import { shallow, ShallowWrapper } from 'enzyme';
|
import { shallow, ShallowWrapper } from 'enzyme';
|
||||||
import { Route } from 'react-router-dom';
|
import { Route } from 'react-router-dom';
|
||||||
import { identity } from 'ramda';
|
import { identity } from 'ramda';
|
||||||
|
import { Mock } from 'ts-mockery';
|
||||||
|
import { Settings } from '../src/settings/reducers/settings';
|
||||||
import appFactory from '../src/App';
|
import appFactory from '../src/App';
|
||||||
|
|
||||||
describe('<App />', () => {
|
describe('<App />', () => {
|
||||||
|
@ -10,7 +12,7 @@ describe('<App />', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const App = appFactory(MainHeader, () => null, () => null, () => null, () => null, () => null, () => null);
|
const App = appFactory(MainHeader, () => null, () => null, () => null, () => null, () => null, () => null);
|
||||||
|
|
||||||
wrapper = shallow(<App fetchServers={identity} servers={{}} />);
|
wrapper = shallow(<App fetchServers={identity} servers={{}} settings={Mock.all<Settings>()} />);
|
||||||
});
|
});
|
||||||
afterEach(() => wrapper.unmount());
|
afterEach(() => wrapper.unmount());
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,7 @@ describe('<ShortUrlCreation />', () => {
|
||||||
[{ validateUrls: true }, true ],
|
[{ validateUrls: true }, true ],
|
||||||
[{ validateUrls: false }, false ],
|
[{ validateUrls: false }, false ],
|
||||||
[ undefined, false ],
|
[ undefined, false ],
|
||||||
])('switch is toggled if option is tru', (shortUrlCreation, expectedChecked) => {
|
])('switch is toggled if option is true', (shortUrlCreation, expectedChecked) => {
|
||||||
const wrapper = createWrapper(shortUrlCreation);
|
const wrapper = createWrapper(shortUrlCreation);
|
||||||
const toggle = wrapper.find(ToggleSwitch);
|
const toggle = wrapper.find(ToggleSwitch);
|
||||||
|
|
||||||
|
|
60
test/settings/UserInterface.test.tsx
Normal file
60
test/settings/UserInterface.test.tsx
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
import { shallow, ShallowWrapper } from 'enzyme';
|
||||||
|
import { Mock } from 'ts-mockery';
|
||||||
|
import { faMoon, faSun } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import { Settings, UiSettings } from '../../src/settings/reducers/settings';
|
||||||
|
import { UserInterface } from '../../src/settings/UserInterface';
|
||||||
|
import ToggleSwitch from '../../src/utils/ToggleSwitch';
|
||||||
|
import { Theme } from '../../src/utils/theme';
|
||||||
|
|
||||||
|
describe('<UserInterface />', () => {
|
||||||
|
let wrapper: ShallowWrapper;
|
||||||
|
const setUiSettings = jest.fn();
|
||||||
|
const createWrapper = (ui?: UiSettings) => {
|
||||||
|
wrapper = shallow(
|
||||||
|
<UserInterface
|
||||||
|
settings={Mock.of<Settings>({ ui })}
|
||||||
|
setUiSettings={setUiSettings}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
return wrapper;
|
||||||
|
};
|
||||||
|
|
||||||
|
afterEach(() => wrapper?.unmount());
|
||||||
|
afterEach(jest.clearAllMocks);
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
[{ theme: 'dark' as Theme }, true ],
|
||||||
|
[{ theme: 'light' as Theme }, false ],
|
||||||
|
[ undefined, false ],
|
||||||
|
])('toggles switch if theme is dark', (ui, expectedChecked) => {
|
||||||
|
const wrapper = createWrapper(ui);
|
||||||
|
const toggle = wrapper.find(ToggleSwitch);
|
||||||
|
|
||||||
|
expect(toggle.prop('checked')).toEqual(expectedChecked);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
[{ theme: 'dark' as Theme }, faMoon ],
|
||||||
|
[{ theme: 'light' as Theme }, faSun ],
|
||||||
|
[ undefined, faSun ],
|
||||||
|
])('shows different icons based on theme', (ui, expectedIcon) => {
|
||||||
|
const wrapper = createWrapper(ui);
|
||||||
|
const icon = wrapper.find(FontAwesomeIcon);
|
||||||
|
|
||||||
|
expect(icon.prop('icon')).toEqual(expectedIcon);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
[ true, 'dark' ],
|
||||||
|
[ false, 'light' ],
|
||||||
|
])('invokes setUiSettings when toggle value changes', (checked, theme) => {
|
||||||
|
const wrapper = createWrapper();
|
||||||
|
const toggle = wrapper.find(ToggleSwitch);
|
||||||
|
|
||||||
|
expect(setUiSettings).not.toHaveBeenCalled();
|
||||||
|
toggle.simulate('change', checked);
|
||||||
|
expect(setUiSettings).toHaveBeenCalledWith({ theme });
|
||||||
|
});
|
||||||
|
});
|
|
@ -3,12 +3,14 @@ import reducer, {
|
||||||
toggleRealTimeUpdates,
|
toggleRealTimeUpdates,
|
||||||
setRealTimeUpdatesInterval,
|
setRealTimeUpdatesInterval,
|
||||||
setShortUrlCreationSettings,
|
setShortUrlCreationSettings,
|
||||||
|
setUiSettings,
|
||||||
} from '../../../src/settings/reducers/settings';
|
} from '../../../src/settings/reducers/settings';
|
||||||
|
|
||||||
describe('settingsReducer', () => {
|
describe('settingsReducer', () => {
|
||||||
const realTimeUpdates = { enabled: true };
|
const realTimeUpdates = { enabled: true };
|
||||||
const shortUrlCreation = { validateUrls: false };
|
const shortUrlCreation = { validateUrls: false };
|
||||||
const settings = { realTimeUpdates, shortUrlCreation };
|
const ui = { theme: 'light' };
|
||||||
|
const settings = { realTimeUpdates, shortUrlCreation, ui };
|
||||||
|
|
||||||
describe('reducer', () => {
|
describe('reducer', () => {
|
||||||
it('returns realTimeUpdates when action is SET_SETTINGS', () => {
|
it('returns realTimeUpdates when action is SET_SETTINGS', () => {
|
||||||
|
@ -39,4 +41,12 @@ describe('settingsReducer', () => {
|
||||||
expect(result).toEqual({ type: SET_SETTINGS, shortUrlCreation: { validateUrls: true } });
|
expect(result).toEqual({ type: SET_SETTINGS, shortUrlCreation: { validateUrls: true } });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('setUiSettings', () => {
|
||||||
|
it('creates action to set ui settings', () => {
|
||||||
|
const result = setUiSettings({ theme: 'dark' });
|
||||||
|
|
||||||
|
expect(result).toEqual({ type: SET_SETTINGS, ui: { theme: 'dark' } });
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { shallow, ShallowWrapper } from 'enzyme';
|
import { shallow, ShallowWrapper } from 'enzyme';
|
||||||
import { Modal } from 'reactstrap';
|
|
||||||
import { Mock } from 'ts-mockery';
|
import { Mock } from 'ts-mockery';
|
||||||
import createEditTagsModal from '../../../src/short-urls/helpers/EditTagsModal';
|
import createEditTagsModal from '../../../src/short-urls/helpers/EditTagsModal';
|
||||||
import { ShortUrl } from '../../../src/short-urls/data';
|
import { ShortUrl } from '../../../src/short-urls/data';
|
||||||
import { ShortUrlTags } from '../../../src/short-urls/reducers/shortUrlTags';
|
import { ShortUrlTags } from '../../../src/short-urls/reducers/shortUrlTags';
|
||||||
import { OptionalString } from '../../../src/utils/utils';
|
import { OptionalString } from '../../../src/utils/utils';
|
||||||
|
import { Modal } from 'reactstrap';
|
||||||
|
|
||||||
describe('<EditTagsModal />', () => {
|
describe('<EditTagsModal />', () => {
|
||||||
let wrapper: ShallowWrapper;
|
let wrapper: ShallowWrapper;
|
||||||
|
|
|
@ -85,7 +85,7 @@ describe('<VisitsTable />', () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(wrapper.find('.text-primary')).toHaveLength(3);
|
expect(wrapper.find('.text-primary')).toHaveLength(3);
|
||||||
expect(wrapper.find('.table-primary')).toHaveLength(2);
|
expect(wrapper.find('.table-active')).toHaveLength(2);
|
||||||
|
|
||||||
// Select one extra
|
// Select one extra
|
||||||
wrapper.find('tr').at(5).simulate('click');
|
wrapper.find('tr').at(5).simulate('click');
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { shallow, ShallowWrapper } from 'enzyme';
|
import { shallow, ShallowWrapper } from 'enzyme';
|
||||||
import { Modal } from 'reactstrap';
|
|
||||||
import { Marker, Popup } from 'react-leaflet';
|
import { Marker, Popup } from 'react-leaflet';
|
||||||
import MapModal from '../../../src/visits/helpers/MapModal';
|
import MapModal from '../../../src/visits/helpers/MapModal';
|
||||||
import { CityStats } from '../../../src/visits/types';
|
import { CityStats } from '../../../src/visits/types';
|
||||||
|
import { Modal } from 'reactstrap';
|
||||||
|
|
||||||
describe('<MapModal />', () => {
|
describe('<MapModal />', () => {
|
||||||
let wrapper: ShallowWrapper;
|
let wrapper: ShallowWrapper;
|
||||||
|
|
Loading…
Reference in a new issue