Theme and language toggle

This commit is contained in:
Corentin Risselin 2020-01-01 23:14:26 +09:00
commit f11aec082d
14 changed files with 252 additions and 89 deletions

View file

@ -1,7 +1,11 @@
:root :root
{ {
--main-bg-color: #333; --main-bg-color: #21262b;
--main-fg-color: #eee; --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%); --main-primary-color: hsl(213, 35%, 65%);
--light-primary-color: hsl(213, 35%, 45%); --light-primary-color: hsl(213, 35%, 45%);

View file

@ -1,7 +1,11 @@
:root :root
{ {
--main-bg-color: #eee; --main-bg-color: #ddd;
--main-fg-color: #333; --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%); --main-primary-color: hsl(213, 35%, 45%);
--light-primary-color: hsl(213, 35%, 65%); --light-primary-color: hsl(213, 35%, 65%);

View file

@ -19,6 +19,7 @@
"webpack-cli": "^3.3.10" "webpack-cli": "^3.3.10"
}, },
"devDependencies": { "devDependencies": {
"copy-webpack-plugin": "^5.1.1",
"css-loader": "^3.4.0", "css-loader": "^3.4.0",
"file-loader": "^5.0.2", "file-loader": "^5.0.2",
"style-loader": "^1.1.2", "style-loader": "^1.1.2",

View file

@ -11,7 +11,8 @@ const server = http.createServer((request, response) => {
'.js': 'text/javascript', '.js': 'text/javascript',
'.gz': 'application/javascript', '.gz': 'application/javascript',
'.svg': 'image/svg+xml', '.svg': 'image/svg+xml',
'.css': 'text/css' '.css': 'text/css',
'.json': 'application/json'
}; };
const mimeEncoding = { const mimeEncoding = {
'.gz': 'gzip', '.gz': 'gzip',

View file

@ -1,8 +1,50 @@
export const themeActions = { export const themeActions = {
THEME_CHANGE: 'THEME_CHANGE' THEME_CHANGE: 'THEME_CHANGE'
} }
export const changeTheme = (new_theme) => ({ export const changeTheme = (new_theme) => ({
type: themeActions.THEME_CHANGE, type: themeActions.THEME_CHANGE,
theme: new_theme 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))
})
}
}

View file

@ -3,7 +3,8 @@ import { render } from 'inferno'
import { Provider } from 'inferno-redux'; import { Provider } from 'inferno-redux';
import { applyMiddleware, createStore, combineReducers } from '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' import Main from './main.jsx'
@ -16,20 +17,43 @@ const logger = process.env.DEBUG ? store => next => action => {
return result return result
} : null } : null
const persistedState = localStorage.getItem('reduxState') const thunk = store => next => action =>
const initState = persistedState ? JSON.parse(persistedState) : {} typeof action === 'function'
? action(store.dispatch, store.getState)
: next(action)
const reducers = combineReducers( const persistedState = {
{theme: themeReducer}) theme: localStorage.getItem('theme') || themeReducer(undefined, {type: null}),
const store = process.env.DEBUG ? lang: localStorage.getItem('lang') ?
createStore(reducers, initState, applyMiddleware(logger)) : createStore(reducers, initState) {...langReducer(undefined, {type: null}), lang: localStorage.getItem('lang')} :
langReducer(undefined, {type: null})
}
//const initState = persistedState ? JSON.parse(persistedState) : {}
store.subscribe(() => { const reducers = combineReducers({
localStorage.setItem('reduxState', JSON.stringify(store.getState())) theme: themeReducer,
lang: langReducer
}) })
render( const store = createStore(
<Provider store={store}> reducers,
<Main /> persistedState,
</Provider>, process.env.DEBUG ? applyMiddleware(thunk, logger) : applyMiddleware(thunk))
document.getElementById("root"))
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
View file

@ -0,0 +1,4 @@
{
"lang": "English",
"test": "Cloud"
}

4
src/lang/jp.json Normal file
View file

@ -0,0 +1,4 @@
{
"lang": "日本語",
"test": "雲"
}

View file

@ -10,68 +10,52 @@ body
margin: 0; margin: 0;
} }
h1, h2, h3, h4, h5, h5
{
margin: 0;
}
nav nav
{ {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 20px; padding: 10px;
background-color: var(--lighterbg-color);
border-bottom: 1px solid var(--highlight-bg-color);
} }
/* START switch */ nav .actions
input:checked + .slider::before {
transform: translateX(20px);
}
.switch input {
display: none;
}
.slider.round
{ {
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; cursor: pointer;
top: 0; padding: 4px;
left: 0; border-radius: 12px;
right: 0; transition-duration: 0.3s;
bottom: 0;
background-color: var(--light-primary-color);
border: 1px solid var(--light-primary-color);
} }
.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 #ayo_logo
{ {
width: 200px; height: 42px;
fill: var(--main-fg-color);
} }

View file

@ -1,12 +1,16 @@
`use strict` `use strict`
import { Component } from 'inferno' 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 { Svg } from './svg.jsx'
import { SvgToggle } from './svg_toggle.jsx'
import './main.css' 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 class MainComponent extends Component
{ {
@ -23,9 +27,14 @@ class MainComponent extends Component
500) 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() render()
@ -34,24 +43,28 @@ class MainComponent extends Component
<div> <div>
<link rel="stylesheet" type="text/css" href={'/assets/theme/' + this.props.theme + '.css'} /> <link rel="stylesheet" type="text/css" href={'/assets/theme/' + this.props.theme + '.css'} />
<nav> <nav>
<Svg url={Logo}/> <Svg url={LogoSvg}/>
<label className="switch" htmlFor="switch-theme"> <h1>{this.props.strings.test}</h1>
<input type="checkbox" id="switch-theme" <div className="actions">
defaultChecked={this.props.theme == 'dark'} <SvgToggle default={LangSvg} text={this.props.strings.lang}
onChange={(event) => this.toggleTheme(event)}/> toggle={(checked) => this.toggleLang(checked)}/>
<div className="slider round"></div> <SvgToggle default={LightSvg} checked={DarkSvg}
</label> toggle={(checked) => this.toggleTheme(checked)} value={this.props.theme == 'dark'}/>
</div>
</nav> </nav>
</div>) </div>)
} }
} }
const mapStateToProps = state => ({ const mapStateToProps = state => ({
strings: state.lang.strings,
lang: state.lang,
theme: state.theme theme: state.theme
}) })
const mapDispatchToProps = dispatch => ({ const mapDispatchToProps = dispatch => ({
changeTheme: theme => dispatch(changeTheme(theme)) changeTheme: theme => dispatch(changeTheme(theme)),
fetchLang: lang => dispatch(fetchLang(lang))
}) })
export default connect( export default connect(

View file

@ -1,11 +1,31 @@
import { themeActions } from './actions.js' import { langActions, themeActions } from './actions.js'
export const themeReducer = (state = 'light', action) => { export const themeReducer = (state = 'light', action) => {
switch (action.type) switch (action.type)
{ {
case themeActions.THEME_CHANGE: case themeActions.THEME_CHANGE:
return action.theme return action.theme
default: default:
return state 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
View 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 || '') }}/>
}
}

View file

@ -1,5 +1,6 @@
const path = require('path'); const path = require('path');
const webpack = require('webpack'); const webpack = require('webpack');
const CopyPlugin = require('copy-webpack-plugin');
const CompressionPlugin = require("compression-webpack-plugin") const CompressionPlugin = require("compression-webpack-plugin")
const config = require('./webpack.base.js'); const config = require('./webpack.base.js');
@ -30,6 +31,9 @@ module.exports = Object.assign(
'DEBUG': true 'DEBUG': true
} // set a DEBUG flag that can be used in the scripts } // set a DEBUG flag that can be used in the scripts
}), }),
new CopyPlugin([
{from: 'src/lang', to: 'lang'}
]),
new CompressionPlugin( new CompressionPlugin(
{ {
filename: "[path].gz[query]", filename: "[path].gz[query]",

View file

@ -1,4 +1,5 @@
const webpack = require('webpack'); const webpack = require('webpack');
const CopyPlugin = require('copy-webpack-plugin');
const CompressionPlugin = require("compression-webpack-plugin") const CompressionPlugin = require("compression-webpack-plugin")
const config = require('./webpack.base.js'); const config = require('./webpack.base.js');
@ -18,6 +19,9 @@ module.exports = Object.assign(
'DEBUG': false 'DEBUG': false
} // set a DEBUG flag that can be used in the scripts (can be skipped) } // set a DEBUG flag that can be used in the scripts (can be skipped)
}), }),
new CopyPlugin([
{from: 'src/lang', to: 'lang'}
]),
new CompressionPlugin( new CompressionPlugin(
{ {
filename: "[path].gz[query]", filename: "[path].gz[query]",