Theme and language toggle
This commit is contained in:
parent
56d40196d6
commit
f11aec082d
14 changed files with 252 additions and 89 deletions
|
|
@ -1,7 +1,11 @@
|
|||
:root
|
||||
{
|
||||
--main-bg-color: #333;
|
||||
--main-fg-color: #eee;
|
||||
--main-bg-color: #21262b;
|
||||
--main-fg-color: #bbb;
|
||||
--lighter-bg-color: #31363b;
|
||||
--lighter-fg-color: #ccc;
|
||||
--highlight-bg-color: #41464b;
|
||||
--highlight-fg-color: #ddd;
|
||||
|
||||
--main-primary-color: hsl(213, 35%, 65%);
|
||||
--light-primary-color: hsl(213, 35%, 45%);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
:root
|
||||
{
|
||||
--main-bg-color: #eee;
|
||||
--main-fg-color: #333;
|
||||
--main-bg-color: #ddd;
|
||||
--main-fg-color: #21262b;
|
||||
--lighter-bg-color: #ccc;
|
||||
--lighter-fg-color: #31363b;
|
||||
--highlight-bg-color: #bbb;
|
||||
--highlight-fg-color: #41464b;
|
||||
|
||||
--main-primary-color: hsl(213, 35%, 45%);
|
||||
--light-primary-color: hsl(213, 35%, 65%);
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
"webpack-cli": "^3.3.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"copy-webpack-plugin": "^5.1.1",
|
||||
"css-loader": "^3.4.0",
|
||||
"file-loader": "^5.0.2",
|
||||
"style-loader": "^1.1.2",
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@ const server = http.createServer((request, response) => {
|
|||
'.js': 'text/javascript',
|
||||
'.gz': 'application/javascript',
|
||||
'.svg': 'image/svg+xml',
|
||||
'.css': 'text/css'
|
||||
'.css': 'text/css',
|
||||
'.json': 'application/json'
|
||||
};
|
||||
const mimeEncoding = {
|
||||
'.gz': 'gzip',
|
||||
|
|
|
|||
|
|
@ -6,3 +6,45 @@ export const changeTheme = (new_theme) => ({
|
|||
type: themeActions.THEME_CHANGE,
|
||||
theme: new_theme
|
||||
})
|
||||
|
||||
export const langActions = {
|
||||
LANG_CHANGE_REQUEST: 'LANG_CHANGE_REQUEST',
|
||||
LANG_CHANGE_RESPONSE: 'LANG_CHANGE_RESPONSE'
|
||||
}
|
||||
|
||||
export const changeLang = (new_lang) => ({
|
||||
type: langActions.LANG_CHANGE_REQUEST,
|
||||
lang: new_lang
|
||||
})
|
||||
|
||||
export const receiveLang = (new_lang, strings) => ({
|
||||
type: langActions.LANG_CHANGE_RESPONSE,
|
||||
lang: new_lang,
|
||||
strings: strings
|
||||
})
|
||||
|
||||
export const fetchLang = (new_lang) => {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState()
|
||||
if(state.lang.strings !== null && state.lang.lang === new_lang)
|
||||
{
|
||||
return
|
||||
}
|
||||
dispatch(changeLang(new_lang))
|
||||
return fetch('/public/lang/' + new_lang + '.json')
|
||||
.then((response) => {
|
||||
if(!response.ok)
|
||||
{
|
||||
console.log('Couldn\'t fetch ' + new_lang + '.json')
|
||||
throw Error(response.statusText)
|
||||
}
|
||||
return response.json()
|
||||
})
|
||||
.then(json => {
|
||||
dispatch(receiveLang(new_lang, json));
|
||||
})
|
||||
.catch(() => {
|
||||
dispatch(changeLang(null))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,8 @@ import { render } from 'inferno'
|
|||
import { Provider } from 'inferno-redux';
|
||||
import { applyMiddleware, createStore, combineReducers } from 'redux';
|
||||
|
||||
import { themeReducer } from './reducers';
|
||||
import { langReducer, themeReducer } from './reducers';
|
||||
import { fetchLang } from './actions'
|
||||
|
||||
import Main from './main.jsx'
|
||||
|
||||
|
|
@ -16,20 +17,43 @@ const logger = process.env.DEBUG ? store => next => action => {
|
|||
return result
|
||||
} : null
|
||||
|
||||
const persistedState = localStorage.getItem('reduxState')
|
||||
const initState = persistedState ? JSON.parse(persistedState) : {}
|
||||
const thunk = store => next => action =>
|
||||
typeof action === 'function'
|
||||
? action(store.dispatch, store.getState)
|
||||
: next(action)
|
||||
|
||||
const reducers = combineReducers(
|
||||
{theme: themeReducer})
|
||||
const store = process.env.DEBUG ?
|
||||
createStore(reducers, initState, applyMiddleware(logger)) : createStore(reducers, initState)
|
||||
const persistedState = {
|
||||
theme: localStorage.getItem('theme') || themeReducer(undefined, {type: null}),
|
||||
lang: localStorage.getItem('lang') ?
|
||||
{...langReducer(undefined, {type: null}), lang: localStorage.getItem('lang')} :
|
||||
langReducer(undefined, {type: null})
|
||||
}
|
||||
//const initState = persistedState ? JSON.parse(persistedState) : {}
|
||||
|
||||
store.subscribe(() => {
|
||||
localStorage.setItem('reduxState', JSON.stringify(store.getState()))
|
||||
const reducers = combineReducers({
|
||||
theme: themeReducer,
|
||||
lang: langReducer
|
||||
})
|
||||
|
||||
const store = createStore(
|
||||
reducers,
|
||||
persistedState,
|
||||
process.env.DEBUG ? applyMiddleware(thunk, logger) : applyMiddleware(thunk))
|
||||
|
||||
let savedState = {
|
||||
theme: store.getState().theme,
|
||||
lang: store.getState().lang.lang
|
||||
}
|
||||
store.subscribe(() => {
|
||||
const state = store.getState()
|
||||
if(savedState.theme !== state.theme) localStorage.setItem('theme', store.getState().theme)
|
||||
if(savedState.lang !== state.lang.lang) localStorage.setItem('lang', store.getState().lang.lang)
|
||||
})
|
||||
|
||||
store.dispatch(fetchLang(store.getState().lang.lang)).then(() => {
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<Main />
|
||||
</Provider>,
|
||||
document.getElementById("root"))
|
||||
})
|
||||
|
|
|
|||
4
src/lang/en.json
Normal file
4
src/lang/en.json
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"lang": "English",
|
||||
"test": "Cloud"
|
||||
}
|
||||
4
src/lang/jp.json
Normal file
4
src/lang/jp.json
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"lang": "日本語",
|
||||
"test": "雲"
|
||||
}
|
||||
74
src/main.css
74
src/main.css
|
|
@ -10,68 +10,52 @@ body
|
|||
margin: 0;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h5
|
||||
{
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
nav
|
||||
{
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
padding: 10px;
|
||||
background-color: var(--lighterbg-color);
|
||||
border-bottom: 1px solid var(--highlight-bg-color);
|
||||
}
|
||||
|
||||
/* START switch */
|
||||
input:checked + .slider::before {
|
||||
transform: translateX(20px);
|
||||
}
|
||||
|
||||
.switch input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.slider.round
|
||||
nav .actions
|
||||
{
|
||||
border-radius: 34px;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.slider
|
||||
svg
|
||||
{
|
||||
position: absolute;
|
||||
fill: var(--main-fg-color);
|
||||
}
|
||||
|
||||
.toggle
|
||||
{
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: var(--light-primary-color);
|
||||
border: 1px solid var(--light-primary-color);
|
||||
padding: 4px;
|
||||
border-radius: 12px;
|
||||
transition-duration: 0.3s;
|
||||
}
|
||||
|
||||
.slider.round::before
|
||||
.toggle:hover
|
||||
{
|
||||
border-radius: 50%;
|
||||
background-color: var(--highlight-bg-color);
|
||||
color: var(--highlight-fg-color);
|
||||
fill: var(--highlight-fg-color);
|
||||
}
|
||||
|
||||
.slider::before
|
||||
{
|
||||
background-color: var(--main-primary-color);
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 15px;
|
||||
width: 15px;
|
||||
left: 2px;
|
||||
bottom: 1px;
|
||||
transition: background-color 1s, transform .4s;
|
||||
}
|
||||
|
||||
.switch
|
||||
{
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 40px;
|
||||
height: 19px;
|
||||
}
|
||||
/* END switch */
|
||||
|
||||
#ayo_logo
|
||||
{
|
||||
width: 200px;
|
||||
fill: var(--main-fg-color);
|
||||
height: 42px;
|
||||
}
|
||||
39
src/main.jsx
39
src/main.jsx
|
|
@ -1,12 +1,16 @@
|
|||
`use strict`
|
||||
import { Component } from 'inferno'
|
||||
import { connect } from 'inferno-redux';
|
||||
import { connect } from 'inferno-redux'
|
||||
|
||||
import { changeTheme } from './actions';
|
||||
import { changeTheme, fetchLang } from './actions'
|
||||
import { Svg } from './svg.jsx'
|
||||
import { SvgToggle } from './svg_toggle.jsx'
|
||||
|
||||
import './main.css'
|
||||
import Logo from '../assets/logo.svg'
|
||||
import LogoSvg from '../assets/logo.svg'
|
||||
import LangSvg from '../assets/translate.svg'
|
||||
import LightSvg from '../assets/brightness_high.svg'
|
||||
import DarkSvg from '../assets/brightness_medium.svg'
|
||||
|
||||
class MainComponent extends Component
|
||||
{
|
||||
|
|
@ -23,9 +27,14 @@ class MainComponent extends Component
|
|||
500)
|
||||
}
|
||||
|
||||
toggleTheme(event)
|
||||
toggleTheme(checked)
|
||||
{
|
||||
this.props.changeTheme(event.target.checked ? 'dark' : 'light')
|
||||
this.props.changeTheme(checked ? 'dark' : 'light')
|
||||
}
|
||||
|
||||
toggleLang(checked)
|
||||
{
|
||||
this.props.fetchLang(checked ? 'en' : 'jp')
|
||||
}
|
||||
|
||||
render()
|
||||
|
|
@ -34,24 +43,28 @@ class MainComponent extends Component
|
|||
<div>
|
||||
<link rel="stylesheet" type="text/css" href={'/assets/theme/' + this.props.theme + '.css'} />
|
||||
<nav>
|
||||
<Svg url={Logo}/>
|
||||
<label className="switch" htmlFor="switch-theme">
|
||||
<input type="checkbox" id="switch-theme"
|
||||
defaultChecked={this.props.theme == 'dark'}
|
||||
onChange={(event) => this.toggleTheme(event)}/>
|
||||
<div className="slider round"></div>
|
||||
</label>
|
||||
<Svg url={LogoSvg}/>
|
||||
<h1>{this.props.strings.test}</h1>
|
||||
<div className="actions">
|
||||
<SvgToggle default={LangSvg} text={this.props.strings.lang}
|
||||
toggle={(checked) => this.toggleLang(checked)}/>
|
||||
<SvgToggle default={LightSvg} checked={DarkSvg}
|
||||
toggle={(checked) => this.toggleTheme(checked)} value={this.props.theme == 'dark'}/>
|
||||
</div>
|
||||
</nav>
|
||||
</div>)
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
strings: state.lang.strings,
|
||||
lang: state.lang,
|
||||
theme: state.theme
|
||||
})
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
changeTheme: theme => dispatch(changeTheme(theme))
|
||||
changeTheme: theme => dispatch(changeTheme(theme)),
|
||||
fetchLang: lang => dispatch(fetchLang(lang))
|
||||
})
|
||||
|
||||
export default connect(
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { themeActions } from './actions.js'
|
||||
import { langActions, themeActions } from './actions.js'
|
||||
|
||||
export const themeReducer = (state = 'light', action) => {
|
||||
switch (action.type)
|
||||
|
|
@ -9,3 +9,23 @@ export const themeReducer = (state = 'light', action) => {
|
|||
return state
|
||||
}
|
||||
}
|
||||
|
||||
export const langReducer = (state = {lang: 'jp', request: null, strings: null}, action) => {
|
||||
switch (action.type)
|
||||
{
|
||||
case langActions.LANG_CHANGE_REQUEST:
|
||||
return {
|
||||
...state,
|
||||
request: action.lang
|
||||
}
|
||||
case langActions.LANG_CHANGE_RESPONSE:
|
||||
return {
|
||||
...state,
|
||||
lang: action.lang,
|
||||
request: null,
|
||||
strings: action.strings
|
||||
}
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
54
src/svg_toggle.jsx
Normal file
54
src/svg_toggle.jsx
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
`use strict`
|
||||
import { Component } from 'inferno'
|
||||
|
||||
export class SvgToggle extends Component
|
||||
{
|
||||
constructor(props)
|
||||
{
|
||||
super(props)
|
||||
this.state = {
|
||||
checked: props.value !== undefined ? props.value : false,
|
||||
svg_default: null,
|
||||
svg_checked: null
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount()
|
||||
{
|
||||
fetch(this.props.default)
|
||||
.then(res => res.text())
|
||||
.then(text => {
|
||||
if(this.props.checked)
|
||||
{
|
||||
this.setState({ svg_default: text })
|
||||
}
|
||||
else
|
||||
{
|
||||
this.setState({ svg_default: text, svg_checked: text})
|
||||
}
|
||||
})
|
||||
if(this.props.checked)
|
||||
{
|
||||
fetch(this.props.checked)
|
||||
.then(res => res.text())
|
||||
.then(text => this.setState({ svg_checked: text }))
|
||||
}
|
||||
}
|
||||
|
||||
onToggle()
|
||||
{
|
||||
if(this.props.toggle !== null)
|
||||
{
|
||||
this.props.toggle(!this.state.checked)
|
||||
}
|
||||
this.setState((state, props) => ({checked: !state.checked}))
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
return <span className="toggle" onClick={() => this.onToggle()}
|
||||
dangerouslySetInnerHTML={
|
||||
{ __html: (this.state.checked ? this.state.svg_checked : this.state.svg_default) +
|
||||
(this.props.text || '') }}/>
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
const path = require('path');
|
||||
const webpack = require('webpack');
|
||||
const CopyPlugin = require('copy-webpack-plugin');
|
||||
const CompressionPlugin = require("compression-webpack-plugin")
|
||||
const config = require('./webpack.base.js');
|
||||
|
||||
|
|
@ -30,6 +31,9 @@ module.exports = Object.assign(
|
|||
'DEBUG': true
|
||||
} // set a DEBUG flag that can be used in the scripts
|
||||
}),
|
||||
new CopyPlugin([
|
||||
{from: 'src/lang', to: 'lang'}
|
||||
]),
|
||||
new CompressionPlugin(
|
||||
{
|
||||
filename: "[path].gz[query]",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
const webpack = require('webpack');
|
||||
const CopyPlugin = require('copy-webpack-plugin');
|
||||
const CompressionPlugin = require("compression-webpack-plugin")
|
||||
const config = require('./webpack.base.js');
|
||||
|
||||
|
|
@ -18,6 +19,9 @@ module.exports = Object.assign(
|
|||
'DEBUG': false
|
||||
} // set a DEBUG flag that can be used in the scripts (can be skipped)
|
||||
}),
|
||||
new CopyPlugin([
|
||||
{from: 'src/lang', to: 'lang'}
|
||||
]),
|
||||
new CompressionPlugin(
|
||||
{
|
||||
filename: "[path].gz[query]",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue