mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2026-04-23 15:06:17 +00:00
Added components and logic to dynamically change theme
This commit is contained in:
committed by
Alejandro Celaya
parent
f313a39b81
commit
9dbf790cc8
@@ -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 (
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
26
src/settings/UserInterface.tsx
Normal file
26
src/settings/UserInterface.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { FC } from 'react';
|
||||||
|
import { SimpleCard } from '../utils/SimpleCard';
|
||||||
|
import ToggleSwitch from '../utils/ToggleSwitch';
|
||||||
|
import { changeThemeInMarkup, Theme } from '../utils/theme';
|
||||||
|
import { Settings, UiSettings } from './reducers/settings';
|
||||||
|
|
||||||
|
interface UserInterfaceProps {
|
||||||
|
settings: Settings;
|
||||||
|
setUiSettings: (settings: UiSettings) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UserInterface: FC<UserInterfaceProps> = ({ settings: { ui }, setUiSettings }) => (
|
||||||
|
<SimpleCard title="User interface">
|
||||||
|
<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;
|
||||||
|
|||||||
@@ -5,3 +5,11 @@ 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 type Theme = 'dark' | 'light';
|
||||||
|
|
||||||
|
export const changeThemeInMarkup = (theme: Theme) => {
|
||||||
|
const html = document.getElementsByTagName('html');
|
||||||
|
|
||||||
|
html?.[0]?.setAttribute('data-theme', theme);
|
||||||
|
};
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|
||||||
|
|||||||
@@ -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' } });
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user