diff --git a/src/App.tsx b/src/App.tsx index 4928f42d..23938617 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,11 +2,14 @@ import { useEffect, FC } from 'react'; import { Route, Switch } from 'react-router-dom'; import NotFound from './common/NotFound'; import { ServersMap } from './servers/data'; +import { Settings } from './settings/reducers/settings'; +import { changeThemeInMarkup } from './utils/theme'; import './App.scss'; interface AppProps { fetchServers: Function; servers: ServersMap; + settings: Settings; } const App = ( @@ -17,12 +20,14 @@ const App = ( EditServer: FC, Settings: FC, ShlinkVersionsContainer: FC, -) => ({ fetchServers, servers }: AppProps) => { - // On first load, try to fetch the remote servers if the list is empty +) => ({ fetchServers, servers, settings }: AppProps) => { useEffect(() => { + // On first load, try to fetch the remote servers if the list is empty if (Object.keys(servers).length === 0) { fetchServers(); } + + changeThemeInMarkup(settings.ui?.theme ?? 'light'); }, []); return ( diff --git a/src/container/index.ts b/src/container/index.ts index f7a0d3cf..b369c1f8 100644 --- a/src/container/index.ts +++ b/src/container/index.ts @@ -43,7 +43,7 @@ bottle.serviceFactory( 'Settings', 'ShlinkVersionsContainer', ); -bottle.decorator('App', connect([ 'servers' ], [ 'fetchServers' ])); +bottle.decorator('App', connect([ 'servers', 'settings' ], [ 'fetchServers' ])); provideCommonServices(bottle, connect, withRouter); provideApiServices(bottle); diff --git a/src/settings/RealTimeUpdates.tsx b/src/settings/RealTimeUpdates.tsx index 914c5a6f..d737f6e9 100644 --- a/src/settings/RealTimeUpdates.tsx +++ b/src/settings/RealTimeUpdates.tsx @@ -19,6 +19,9 @@ const RealTimeUpdates = ( Enable or disable real-time updates, when using Shlink v2.2.0 or newer. + + Real-time updates are currently being {realTimeUpdates.enabled ? 'processed' : 'ignored'}. + diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index bbd524d9..31e6fc6f 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -2,14 +2,19 @@ import { FC } from 'react'; import { Row } from 'reactstrap'; import NoMenuLayout from '../common/NoMenuLayout'; -const Settings = (RealTimeUpdates: FC, ShortUrlCreation: FC) => () => ( +const Settings = (RealTimeUpdates: FC, ShortUrlCreation: FC, UserInterface:FC) => () => (
- +
+ +
+
+ +
- +
diff --git a/src/settings/UserInterface.tsx b/src/settings/UserInterface.tsx new file mode 100644 index 00000000..89597320 --- /dev/null +++ b/src/settings/UserInterface.tsx @@ -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 = ({ settings: { ui }, setUiSettings }) => ( + + { + const theme: Theme = useDarkTheme ? 'dark' : 'light'; + + setUiSettings({ theme }); + changeThemeInMarkup(theme); + }} + > + Use dark theme + + +); diff --git a/src/settings/reducers/settings.ts b/src/settings/reducers/settings.ts index aa1bc929..e5c0d1f9 100644 --- a/src/settings/reducers/settings.ts +++ b/src/settings/reducers/settings.ts @@ -2,6 +2,7 @@ import { Action } from 'redux'; import { dissoc, mergeDeepRight } from 'ramda'; import { buildReducer } from '../../utils/helpers/redux'; import { RecursivePartial } from '../../utils/utils'; +import { Theme } from '../../utils/theme'; export const SET_SETTINGS = 'shlink/realTimeUpdates/SET_SETTINGS'; @@ -19,9 +20,14 @@ export interface ShortUrlCreationSettings { validateUrls: boolean; } +export interface UiSettings { + theme: Theme; +} + export interface Settings { realTimeUpdates: RealTimeUpdatesSettings; shortUrlCreation?: ShortUrlCreationSettings; + ui?: UiSettings; } const initialState: Settings = { @@ -31,6 +37,9 @@ const initialState: Settings = { shortUrlCreation: { validateUrls: false, }, + ui: { + theme: 'light', + }, }; type SettingsAction = Action & Settings; @@ -55,3 +64,8 @@ export const setShortUrlCreationSettings = (settings: ShortUrlCreationSettings): type: SET_SETTINGS, shortUrlCreation: settings, }); + +export const setUiSettings = (settings: UiSettings): PartialSettingsAction => ({ + type: SET_SETTINGS, + ui: settings, +}); diff --git a/src/settings/services/provideServices.ts b/src/settings/services/provideServices.ts index 393ccefc..cd01599b 100644 --- a/src/settings/services/provideServices.ts +++ b/src/settings/services/provideServices.ts @@ -1,14 +1,20 @@ import Bottle from 'bottlejs'; import RealTimeUpdates from '../RealTimeUpdates'; 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 { withoutSelectedServer } from '../../servers/helpers/withoutSelectedServer'; import { ShortUrlCreation } from '../ShortUrlCreation'; +import { UserInterface } from '../UserInterface'; const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { // Components - bottle.serviceFactory('Settings', Settings, 'RealTimeUpdates', 'ShortUrlCreation'); + bottle.serviceFactory('Settings', Settings, 'RealTimeUpdates', 'ShortUrlCreation', 'UserInterface'); bottle.decorator('Settings', withoutSelectedServer); bottle.decorator('Settings', connect(null, [ 'resetSelectedServer' ])); @@ -21,10 +27,14 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { bottle.serviceFactory('ShortUrlCreation', () => ShortUrlCreation); bottle.decorator('ShortUrlCreation', connect([ 'settings' ], [ 'setShortUrlCreationSettings' ])); + bottle.serviceFactory('UserInterface', () => UserInterface); + bottle.decorator('UserInterface', connect([ 'settings' ], [ 'setUiSettings' ])); + // Actions bottle.serviceFactory('toggleRealTimeUpdates', () => toggleRealTimeUpdates); bottle.serviceFactory('setRealTimeUpdatesInterval', () => setRealTimeUpdatesInterval); bottle.serviceFactory('setShortUrlCreationSettings', () => setShortUrlCreationSettings); + bottle.serviceFactory('setUiSettings', () => setUiSettings); }; export default provideServices; diff --git a/src/utils/theme/index.ts b/src/utils/theme/index.ts index a4b5f96b..ca56555b 100644 --- a/src/utils/theme/index.ts +++ b/src/utils/theme/index.ts @@ -5,3 +5,11 @@ export const MAIN_COLOR_ALPHA = 'rgba(70, 150, 229, 0.4)'; export const HIGHLIGHTED_COLOR = '#F77F28'; 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); +}; diff --git a/test/App.test.tsx b/test/App.test.tsx index 26938298..da1fda0b 100644 --- a/test/App.test.tsx +++ b/test/App.test.tsx @@ -1,6 +1,8 @@ import { shallow, ShallowWrapper } from 'enzyme'; import { Route } from 'react-router-dom'; import { identity } from 'ramda'; +import { Mock } from 'ts-mockery'; +import { Settings } from '../src/settings/reducers/settings'; import appFactory from '../src/App'; describe('', () => { @@ -10,7 +12,7 @@ describe('', () => { beforeEach(() => { const App = appFactory(MainHeader, () => null, () => null, () => null, () => null, () => null, () => null); - wrapper = shallow(); + wrapper = shallow(()} />); }); afterEach(() => wrapper.unmount()); diff --git a/test/settings/reducers/settings.test.ts b/test/settings/reducers/settings.test.ts index 1bfd1701..57d22066 100644 --- a/test/settings/reducers/settings.test.ts +++ b/test/settings/reducers/settings.test.ts @@ -3,12 +3,14 @@ import reducer, { toggleRealTimeUpdates, setRealTimeUpdatesInterval, setShortUrlCreationSettings, + setUiSettings, } from '../../../src/settings/reducers/settings'; describe('settingsReducer', () => { const realTimeUpdates = { enabled: true }; const shortUrlCreation = { validateUrls: false }; - const settings = { realTimeUpdates, shortUrlCreation }; + const ui = { theme: 'light' }; + const settings = { realTimeUpdates, shortUrlCreation, ui }; describe('reducer', () => { it('returns realTimeUpdates when action is SET_SETTINGS', () => { @@ -39,4 +41,12 @@ describe('settingsReducer', () => { 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' } }); + }); + }); });