diff --git a/app/components/ThemeToggle/index.js b/app/components/ThemeToggle/index.js new file mode 100644 index 0000000..d492342 --- /dev/null +++ b/app/components/ThemeToggle/index.js @@ -0,0 +1,68 @@ +/** + * + * ThemeToggle Component + * + */ +import React from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { createStructuredSelector } from 'reselect'; +import styled from 'styled-components'; +import { themeActionCreators } from '@app/containers/Theme/reducer'; +import { selectTheme } from '@app/containers/Theme/selectors'; + +const ToggleButton = styled.button` + background: var(--color-accent-muted); + border: 1px solid var(--color-border); + border-radius: 50%; + width: 40px; + height: 40px; + padding: 0; + color: var(--color-text); + cursor: pointer; + font-size: 20px; + line-height: 1; + font-family: inherit; + transition: all var(--transition-fast); + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + + &:hover { + background: var(--color-accent); + color: #fff; + border-color: var(--color-accent); + } + + &:active { + transform: scale(0.98); + } +`; + +export function ThemeToggle({ theme, onToggle }) { + return ( + + {theme === 'dark' ? '☀️' : '🌙'} + + ); +} + +ThemeToggle.propTypes = { + theme: PropTypes.string.isRequired, + onToggle: PropTypes.func.isRequired +}; + +const mapStateToProps = createStructuredSelector({ + theme: selectTheme() +}); + +const mapDispatchToProps = (dispatch) => ({ + onToggle: () => dispatch(themeActionCreators.toggleTheme()) +}); + +export default connect(mapStateToProps, mapDispatchToProps)(ThemeToggle); diff --git a/app/containers/Theme/reducer.js b/app/containers/Theme/reducer.js new file mode 100644 index 0000000..16eb46b --- /dev/null +++ b/app/containers/Theme/reducer.js @@ -0,0 +1,48 @@ +/* + * + * Theme reducer + * + */ +import { createActions } from 'reduxsauce'; +import produce from 'immer'; + +export const THEME_PAYLOAD = { + THEME: 'theme' +}; + +export const initialState = { + theme: 'light' // 'light' or 'dark' +}; + +export const { Types: themeActionTypes, Creators: themeActionCreators } = createActions({ + setTheme: [THEME_PAYLOAD.THEME], + toggleTheme: null +}); + +function persistTheme(theme) { + if (typeof window !== 'undefined') { + localStorage.setItem('theme', theme); + document.documentElement.setAttribute('data-theme', theme); + } +} + +export function themeReducer(state = initialState, action) { + return produce(state, (draft) => { + switch (action.type) { + case themeActionTypes.SET_THEME: + draft.theme = action.theme; + persistTheme(action.theme); + break; + case themeActionTypes.TOGGLE_THEME: { + const newTheme = draft.theme === 'light' ? 'dark' : 'light'; + draft.theme = newTheme; + persistTheme(newTheme); + break; + } + default: + break; + } + }); +} + +export default themeReducer; diff --git a/app/containers/Theme/selectors.js b/app/containers/Theme/selectors.js new file mode 100644 index 0000000..6a5f223 --- /dev/null +++ b/app/containers/Theme/selectors.js @@ -0,0 +1,8 @@ +import { createSelector } from 'reselect'; +import { initialState } from './reducer'; + +const selectThemeDomain = (state) => state.theme || initialState; + +export const selectTheme = () => createSelector(selectThemeDomain, (substate) => substate.theme); + +export const selectIsDarkMode = () => createSelector(selectThemeDomain, (substate) => substate.theme === 'dark');