Cleanup code

This commit is contained in:
Antti Pilto 2018-11-17 16:09:14 +02:00
parent eaf95c395e
commit f6bc13bfb9
15 changed files with 944 additions and 957 deletions

View File

@ -1,4 +1,4 @@
# Kana Quiz 2 # Kana Quiz
Kana Quiz made with React.js. Kana Quiz made with React.js.
See live at https://kana.pro/ See live at https://kana.pro/
@ -8,4 +8,4 @@ Install deps: `npm install`
Development: `npm start` Development: `npm start`
Production: `npm run build` Production: `npm run build`

View File

@ -20,7 +20,7 @@
}, },
"homepage": "https://github.com/anzzstuff/kanaquiz#readme", "homepage": "https://github.com/anzzstuff/kanaquiz#readme",
"browserslist": [ "browserslist": [
"> 0.25%", ">0.25%",
"not dead" "not dead"
], ],
"devDependencies": { "devDependencies": {

View File

@ -7,57 +7,50 @@ import { removeHash } from '../../data/helperFuncs';
const options = {}; const options = {};
class App extends Component { class App extends Component {
constructor(props) { state = { gameState: 'chooseCharacters' };
super(props);
this.state = {
gameState: 'chooseCharacters'
}
this.startGame = this.startGame.bind(this);
this.endGame = this.endGame.bind(this);
}
startGame() { startGame = () => {
this.setState({gameState: 'game'}); this.setState({gameState: 'game'});
} }
endGame() { endGame = () => {
this.setState({gameState: 'chooseCharacters'}); this.setState({gameState: 'chooseCharacters'});
} }
componentWillUpdate(nextProps, nextState) { componentWillUpdate(nextProps, nextState) {
// This is primarily for demo site purposes. Hides #footer when game is on. // This is primarily for demo site purposes. Hides #footer when game is on.
if(document.getElementById('footer')) { if(document.getElementById('footer')) {
if(nextState.gameState=='chooseCharacters') if(nextState.gameState=='chooseCharacters')
document.getElementById('footer').style.display = "block"; document.getElementById('footer').style.display = "block";
else else
document.getElementById('footer').style.display = "none"; document.getElementById('footer').style.display = "none";
}
} }
}
componentWillMount() { componentWillMount() {
if(document.getElementById('footer')) if(document.getElementById('footer'))
document.getElementById('footer').style.display = "block"; document.getElementById('footer').style.display = "block";
} }
render() { render() {
return ( return (
<div> <div>
<Navbar <Navbar
gameState={this.state.gameState} gameState={this.state.gameState}
handleEndGame={this.endGame} handleEndGame={this.endGame}
/> />
<div className="outercontainer"> <div className="outercontainer">
<div className="container game"> <div className="container game">
<GameContainer <GameContainer
gameState={this.state.gameState} gameState={this.state.gameState}
handleStartGame={this.startGame} handleStartGame={this.startGame}
handleEndGame={this.endGame} handleEndGame={this.endGame}
/> />
</div> </div>
</div> </div>
</div> </div>
) )
} }
} }
export default App; export default App;

View File

@ -1,41 +1,41 @@
html { html {
height: 100%; /* height sets are for android chrome background fix */ height: 100%; /* height sets are for android chrome background fix */
min-height: 100%; min-height: 100%;
} }
body { body {
min-height: 100%; min-height: 100%;
font-family: "Trebuchet MS","Lucida Grande","Lucida Sans Unicode","Lucida Sans",Tahoma,sans-serif; font-family: "Trebuchet MS","Lucida Grande","Lucida Sans Unicode","Lucida Sans",Tahoma,sans-serif;
background-color: #e5e5e5; background-color: #e5e5e5;
color: #111; color: #111;
} }
.outercontainer { .outercontainer {
background-color: #f5f5f5; background-color: #f5f5f5;
padding-bottom: 20px; padding-bottom: 20px;
border-bottom: 1px #dadada solid; border-bottom: 1px #dadada solid;
min-height: 500px; /* 690 */ min-height: 500px; /* 690 */
@media (max-width: 768px) { @media (max-width: 768px) {
min-height: auto; min-height: auto;
} }
} }
.container { .container {
margin: 0 auto; margin: 0 auto;
max-width: 968px; max-width: 968px;
} }
.row { .row {
margin: 0; margin: 0;
} }
.login-button { .login-button {
font-size: 1.3em; font-size: 1.3em;
margin-top: 15px; margin-top: 15px;
margin-bottom: 0; margin-bottom: 0;
a { a {
color: #888; color: #888;
} }
} }
.game { .game {
padding-top: 70px; padding-top: 70px;
} }
.glyphicon-none:before { .glyphicon-none:before {
content: "\2122"; content: "\2122";
color: transparent !important; color: transparent !important;
} }

View File

@ -1,55 +1,50 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
class CharacterGroup extends Component { class CharacterGroup extends Component {
constructor(props) { state = { shownChars: '' }
super(props);
this.state = {
shownChars : '',
}
this.changeShownChars = this.changeShownChars.bind(this);
}
changeShownChars(newString, e) { changeShownChars(newString) {
this.setState({shownChars: newString}) this.setState({shownChars: newString})
} }
getShowableCharacters(whichKana) { getShowableCharacters(whichKana) {
let strRomajiCharacters = ''; let strRomajiCharacters = '';
let strKanaCharacters = ''; let strKanaCharacters = '';
Object.keys(this.props.characters).map(function(character) { Object.keys(this.props.characters).map(character => {
strRomajiCharacters+=this.props.characters[character][0]+' · '; strRomajiCharacters+=this.props.characters[character][0]+' · ';
strKanaCharacters+=character+' · '; strKanaCharacters+=character+' · ';
}, this); });
strRomajiCharacters = strRomajiCharacters.slice(0, -2); strRomajiCharacters = strRomajiCharacters.slice(0, -2);
strKanaCharacters = strKanaCharacters.slice(0, -2); strKanaCharacters = strKanaCharacters.slice(0, -2);
if(whichKana=='romaji') return strRomajiCharacters; if(whichKana=='romaji') return strRomajiCharacters;
else return strKanaCharacters; else return strKanaCharacters;
} }
componentWillMount() { componentWillMount() {
this.changeShownChars(this.getShowableCharacters('romaji'));
}
render() {
return (
<div
className={
'choose-row' + (this.props.groupName.endsWith('_a') || this.props.groupName.endsWith('_s') ? ' alt-row' : '')
}
onClick={() => {
this.props.handleToggleSelect(this.props.groupName);
this.changeShownChars(this.getShowableCharacters('romaji')); this.changeShownChars(this.getShowableCharacters('romaji'));
} }}
onMouseDown={()=>this.changeShownChars(this.getShowableCharacters('kana'))}
render() { onMouseOut={()=>this.changeShownChars(this.getShowableCharacters('romaji'))}
return ( onTouchStart={()=>this.changeShownChars(this.getShowableCharacters('kana'))}
<div className={ onTouchEnd={()=>this.changeShownChars(this.getShowableCharacters('romaji'))}
'choose-row' + (this.props.groupName.endsWith('_a') || this.props.groupName.endsWith('_s') ? ' alt-row' : '') >
} <span className={this.props.selected ?
onClick={()=>{ 'glyphicon glyphicon-small glyphicon-check' :
this.props.handleToggleSelect(this.props.groupName); 'glyphicon glyphicon-small glyphicon-unchecked'}></span> {this.state.shownChars}
this.changeShownChars(this.getShowableCharacters('romaji')); </div>
}} );
onMouseDown={()=>this.changeShownChars(this.getShowableCharacters('kana'))} }
onMouseOut={()=>this.changeShownChars(this.getShowableCharacters('romaji'))}
onTouchStart={()=>this.changeShownChars(this.getShowableCharacters('kana'))}
onTouchEnd={()=>this.changeShownChars(this.getShowableCharacters('romaji'))}
>
<span className={this.props.selected ?
'glyphicon glyphicon-small glyphicon-check' :
'glyphicon glyphicon-small glyphicon-unchecked'}></span> {this.state.shownChars}
</div>
);
}
} }
export default CharacterGroup; export default CharacterGroup;

View File

@ -5,279 +5,273 @@ import './ChooseCharacters.scss';
import CharacterGroup from './CharacterGroup'; import CharacterGroup from './CharacterGroup';
class ChooseCharacters extends Component { class ChooseCharacters extends Component {
constructor(props) { state = {
super(props); errMsg : '',
this.state = { selectedGroups: this.props.selectedGroups,
errMsg : '', showAlternatives: [],
selectedGroups: this.props.selectedGroups, showSimilars: [],
showAlternatives: [], startIsVisible: true
showSimilars: [], }
startIsVisible: true
} componentDidMount() {
this.toggleSelect = this.toggleSelect.bind(this); this.testIsStartVisible();
this.startGame = this.startGame.bind(this); window.addEventListener('resize', this.testIsStartVisible);
this.testIsStartVisible = this.testIsStartVisible.bind(this); window.addEventListener('scroll', this.testIsStartVisible);
}
componentWillUnmount() {
window.removeEventListener('resize', this.testIsStartVisible);
window.removeEventListener('scroll', this.testIsStartVisible);
}
componentDidUpdate(prevProps, prevState) {
this.testIsStartVisible();
}
testIsStartVisible = () => {
if(this.startRef) {
const rect = this.startRef.getBoundingClientRect();
if(rect.y > window.innerHeight && this.state.startIsVisible)
this.setState({ startIsVisible: false });
else if(rect.y <= window.innerHeight && !this.state.startIsVisible)
this.setState({ startIsVisible: true });
} }
}
componentDidMount() { scrollToStart() {
this.testIsStartVisible(); if(this.startRef) {
window.addEventListener('resize', this.testIsStartVisible); const rect = this.startRef.getBoundingClientRect();
window.addEventListener('scroll', this.testIsStartVisible); const absTop = rect.top + window.pageYOffset;
const scrollPos = absTop - window.innerHeight + 50;
window.scrollTo(0, scrollPos > 0 ? scrollPos : 0);
} }
}
componentWillUnmount() { getIndex(groupName) {
window.removeEventListener('resize', this.testIsStartVisible); return this.state.selectedGroups.indexOf(groupName);
window.removeEventListener('scroll', this.testIsStartVisible); }
isSelected(groupName) {
return this.getIndex(groupName) > -1 ? true : false;
}
removeSelect(groupName) {
if(this.getIndex(groupName)<0)
return;
let newSelectedGroups = this.state.selectedGroups.slice();
newSelectedGroups.splice(this.getIndex(groupName), 1);
this.setState({selectedGroups: newSelectedGroups});
}
addSelect(groupName) {
this.setState({errMsg: '', selectedGroups: this.state.selectedGroups.concat(groupName)});
}
toggleSelect = groupName => {
if(this.getIndex(groupName) > -1)
this.removeSelect(groupName);
else
this.addSelect(groupName);
}
selectAll(whichKana, altOnly=false, similarOnly=false) {
const thisKana = kanaDictionary[whichKana];
let newSelectedGroups = this.state.selectedGroups.slice();
Object.keys(thisKana).forEach(groupName => {
if(!this.isSelected(groupName) && (
(altOnly && groupName.endsWith('_a')) ||
(similarOnly && groupName.endsWith('_s')) ||
(!altOnly && !similarOnly)
))
newSelectedGroups.push(groupName);
});
this.setState({errMsg: '', selectedGroups: newSelectedGroups});
}
selectNone(whichKana, altOnly=false, similarOnly=false) {
let newSelectedGroups = [];
this.state.selectedGroups.forEach(groupName => {
let mustBeRemoved = false;
Object.keys(kanaDictionary[whichKana]).forEach(removableGroupName => {
if(removableGroupName === groupName && (
(altOnly && groupName.endsWith('_a')) ||
(similarOnly && groupName.endsWith('_s')) ||
(!altOnly && !similarOnly)
))
mustBeRemoved = true;
});
if(!mustBeRemoved)
newSelectedGroups.push(groupName);
});
this.setState({selectedGroups: newSelectedGroups});
}
toggleAlternative(whichKana, postfix) {
let show = postfix == '_a' ? this.state.showAlternatives : this.state.showSimilars;
const idx = show.indexOf(whichKana);
if(idx >= 0)
show.splice(idx, 1);
else
show.push(whichKana)
if(postfix == '_a')
this.setState({showAlternatives: show});
if(postfix == '_s')
this.setState({showSimilars: show});
}
getSelectedAlternatives(whichKana, postfix) {
return this.state.selectedGroups.filter(groupName => {
return groupName.startsWith(whichKana == 'hiragana' ? 'h_' : 'k_') &&
groupName.endsWith(postfix);
}).length;
}
getAmountOfAlternatives(whichKana, postfix) {
return Object.keys(kanaDictionary[whichKana]).filter(groupName => {
return groupName.endsWith(postfix);
}).length;
}
alternativeToggleRow(whichKana, postfix, show) {
let checkBtn = "glyphicon glyphicon-small glyphicon-"
let status;
if(this.getSelectedAlternatives(whichKana, postfix) >= this.getAmountOfAlternatives(whichKana, postfix))
status = 'check';
else if(this.getSelectedAlternatives(whichKana, postfix) > 0)
status = 'check half';
else
status = 'unchecked'
checkBtn += status
return <div
key={'alt_toggle_' + whichKana + postfix}
onClick={() => this.toggleAlternative(whichKana, postfix)}
className="choose-row"
>
<span
className={checkBtn}
onClick={ e => {
if(status == 'check')
this.selectNone(whichKana, postfix == '_a', postfix == '_s');
else if(status == 'check half' || status == 'unchecked')
this.selectAll(whichKana, postfix == '_a', postfix == '_s');
e.stopPropagation();
}}
></span>
{
show ? <span className="toggle-caret">&#9650;</span>
: <span className="toggle-caret">&#9660;</span>
}
{
postfix == '_a' ? 'Alternative characters (ga · ba · kya..)' :
'Look-alike characters'
}
</div>
}
showGroupRows(whichKana, showAlternatives, showSimilars = false) {
const thisKana = kanaDictionary[whichKana];
let rows = [];
Object.keys(thisKana).forEach((groupName, idx) => {
if(groupName == "h_group11_a" || groupName == "k_group13_a")
rows.push(this.alternativeToggleRow(whichKana, "_a", showAlternatives));
if(groupName == "k_group11_s")
rows.push(this.alternativeToggleRow(whichKana, "_s", showSimilars));
if((!groupName.endsWith("a") || showAlternatives) &&
(!groupName.endsWith("s") || showSimilars)) {
rows.push(<CharacterGroup
key={idx}
groupName={groupName}
selected={this.isSelected(groupName)}
characters={thisKana[groupName].characters}
handleToggleSelect={this.toggleSelect}
/>);
}
});
return rows;
}
startGame() {
if(this.state.selectedGroups.length < 1) {
this.setState({ errMsg: 'Choose at least one group!'});
return;
} }
this.props.handleStartGame(this.state.selectedGroups);
}
componentDidUpdate(prevProps, prevState) { render() {
this.testIsStartVisible(); return (
} <div className="choose-characters">
<div className="row">
testIsStartVisible() { <div className="col-xs-12">
if(this.startRef) { <div className="panel panel-default">
const rect = this.startRef.getBoundingClientRect(); <div className="panel-body welcome">
if(rect.y > window.innerHeight && this.state.startIsVisible) <h4>Welcome to Kana Pro!</h4>
this.setState({ startIsVisible: false }); <p>Please choose the groups of characters that you'd like to be studying.</p>
else if(rect.y <= window.innerHeight && !this.state.startIsVisible) </div>
this.setState({ startIsVisible: true });
}
}
scrollToStart() {
if(this.startRef) {
const rect = this.startRef.getBoundingClientRect();
const absTop = rect.top + window.pageYOffset;
const scrollPos = absTop - window.innerHeight + 50;
window.scrollTo(0, scrollPos > 0 ? scrollPos : 0);
}
}
getIndex(groupName) {
return this.state.selectedGroups.indexOf(groupName);
}
isSelected(groupName) {
return this.getIndex(groupName) > -1 ? true : false;
}
removeSelect(groupName) {
if(this.getIndex(groupName)<0)
return;
let newSelectedGroups = this.state.selectedGroups.slice();
newSelectedGroups.splice(this.getIndex(groupName), 1);
this.setState({selectedGroups: newSelectedGroups});
}
addSelect(groupName) {
this.setState({errMsg: '', selectedGroups: this.state.selectedGroups.concat(groupName)});
}
toggleSelect(groupName) {
if(this.getIndex(groupName) > -1)
this.removeSelect(groupName);
else
this.addSelect(groupName);
}
selectAll(whichKana, altOnly=false, similarOnly=false) {
const thisKana = kanaDictionary[whichKana];
let newSelectedGroups = this.state.selectedGroups.slice();
Object.keys(thisKana).forEach(groupName => {
if(!this.isSelected(groupName) && (
(altOnly && groupName.endsWith('_a')) ||
(similarOnly && groupName.endsWith('_s')) ||
(!altOnly && !similarOnly)
))
newSelectedGroups.push(groupName);
});
this.setState({errMsg: '', selectedGroups: newSelectedGroups});
}
selectNone(whichKana, altOnly=false, similarOnly=false) {
let newSelectedGroups = [];
this.state.selectedGroups.forEach(groupName => {
let mustBeRemoved = false;
Object.keys(kanaDictionary[whichKana]).forEach(removableGroupName => {
if(removableGroupName === groupName && (
(altOnly && groupName.endsWith('_a')) ||
(similarOnly && groupName.endsWith('_s')) ||
(!altOnly && !similarOnly)
))
mustBeRemoved = true;
});
if(!mustBeRemoved)
newSelectedGroups.push(groupName);
});
this.setState({selectedGroups: newSelectedGroups});
}
toggleAlternative(whichKana, postfix) {
let show = postfix == '_a' ? this.state.showAlternatives : this.state.showSimilars;
const idx = show.indexOf(whichKana);
if(idx >= 0)
show.splice(idx, 1);
else
show.push(whichKana)
if(postfix == '_a')
this.setState({showAlternatives: show});
if(postfix == '_s')
this.setState({showSimilars: show});
}
getSelectedAlternatives(whichKana, postfix) {
return this.state.selectedGroups.filter(groupName => {
return groupName.startsWith(whichKana == 'hiragana' ? 'h_' : 'k_') &&
groupName.endsWith(postfix);
}).length;
}
getAmountOfAlternatives(whichKana, postfix) {
return Object.keys(kanaDictionary[whichKana]).filter(groupName => {
return groupName.endsWith(postfix);
}).length;
}
alternativeToggleRow(whichKana, postfix, show) {
let checkBtn = "glyphicon glyphicon-small glyphicon-"
let status;
if(this.getSelectedAlternatives(whichKana, postfix) >= this.getAmountOfAlternatives(whichKana, postfix))
status = 'check';
else if(this.getSelectedAlternatives(whichKana, postfix) > 0)
status = 'check half';
else
status = 'unchecked'
checkBtn += status
return <div
key={'alt_toggle_' + whichKana + postfix}
onClick={() => this.toggleAlternative(whichKana, postfix)}
className="choose-row"
>
<span
className={checkBtn}
onClick={ e => {
if(status == 'check')
this.selectNone(whichKana, postfix == '_a', postfix == '_s');
else if(status == 'check half' || status == 'unchecked')
this.selectAll(whichKana, postfix == '_a', postfix == '_s');
e.stopPropagation();
}}
></span>
{
show ? <span className="toggle-caret">&#9650;</span>
: <span className="toggle-caret">&#9660;</span>
}
{
postfix == '_a' ? 'Alternative characters (ga · ba · kya..)' :
'Look-alike characters'
}
</div>
}
showGroupRows(whichKana, showAlternatives, showSimilars = false) {
const thisKana = kanaDictionary[whichKana];
let rows = [];
Object.keys(thisKana).forEach((groupName, idx) => {
if(groupName == "h_group11_a" || groupName == "k_group13_a")
rows.push(this.alternativeToggleRow(whichKana, "_a", showAlternatives));
if(groupName == "k_group11_s")
rows.push(this.alternativeToggleRow(whichKana, "_s", showSimilars));
if((!groupName.endsWith("a") || showAlternatives) &&
(!groupName.endsWith("s") || showSimilars)) {
rows.push(<CharacterGroup
key={idx}
groupName={groupName}
selected={this.isSelected(groupName)}
characters={thisKana[groupName].characters}
handleToggleSelect={this.toggleSelect}
/>);
}
});
return rows;
}
startGame() {
if(this.state.selectedGroups.length < 1) {
this.setState({ errMsg: 'Choose at least one group!'});
return;
}
this.props.handleStartGame(this.state.selectedGroups);
}
render() {
return (
<div className="choose-characters">
<div className="row">
<div className="col-xs-12">
<div className="panel panel-default">
<div className="panel-body welcome">
<h4>Welcome to Kana Quiz!</h4>
<p>Please choose the groups of characters that you'd like to be studying.</p>
</div>
</div>
</div>
</div>
<div className="row">
<div className="col-sm-6">
<div className="panel panel-default">
<div className="panel-heading">Hiragana · ひらがな</div>
<div className="panel-body selection-areas">
{this.showGroupRows('hiragana', this.state.showAlternatives.indexOf('hiragana') >= 0)}
</div>
<div className="panel-footer text-center">
<a href="javascript:;" onClick={()=>this.selectAll('hiragana')}>All</a> &nbsp;&middot;&nbsp; <a href="javascript:;"
onClick={()=>this.selectNone('hiragana')}>None</a>
&nbsp;&middot;&nbsp; <a href="javascript:;" onClick={()=>this.selectAll('hiragana', true)}>All alternative</a>
&nbsp;&middot;&nbsp; <a href="javascript:;" onClick={()=>this.selectNone('hiragana', true)}>No alternative</a>
</div>
</div>
</div>
<div className="col-sm-6">
<div className="panel panel-default">
<div className="panel-heading">Katakana · カタカナ</div>
<div className="panel-body selection-areas">
{this.showGroupRows('katakana', this.state.showAlternatives.indexOf('katakana') >= 0, this.state.showSimilars.indexOf('katakana') >= 0)}
</div>
<div className="panel-footer text-center">
<a href="javascript:;" onClick={()=>this.selectAll('katakana')}>All</a> &nbsp;&middot;&nbsp; <a href="javascript:;"
onClick={()=>this.selectNone('katakana')}>None
</a>
&nbsp;&middot;&nbsp; <a href="javascript:;" onClick={()=>this.selectAll('katakana', true)}>All alternative</a>
&nbsp;&middot;&nbsp; <a href="javascript:;" onClick={()=>this.selectNone('katakana', true)}>No alternative</a>
</div>
</div>
</div>
<div className="col-sm-3 col-xs-12 pull-right">
<span className="pull-right lock">Lock to stage &nbsp;
{
this.props.isLocked &&
<input className="stage-choice" type="number" min="1" max="4" maxLength="1" size="1"
onChange={(e)=>this.props.lockStage(e.target.value, true)}
value={this.props.stage}
/>
}
<Switch onClick={()=>this.props.lockStage(1)} on={this.props.isLocked} /></span>
</div>
<div className="col-sm-offset-3 col-sm-6 col-xs-12 text-center">
{
this.state.errMsg != '' &&
<div className="error-message">{this.state.errMsg}</div>
}
<button ref={c => this.startRef = c} className="btn btn-danger startgame-button" onClick={this.startGame}>Start the Quiz!</button>
</div>
<div className="down-arrow"
style={{display: this.state.startIsVisible ? 'none' : 'block'}}
onClick={(e) => this.scrollToStart(e)}
>
Start
</div>
</div>
</div> </div>
); </div>
} </div>
<div className="row">
<div className="col-sm-6">
<div className="panel panel-default">
<div className="panel-heading">Hiragana · ひらがな</div>
<div className="panel-body selection-areas">
{this.showGroupRows('hiragana', this.state.showAlternatives.indexOf('hiragana') >= 0)}
</div>
<div className="panel-footer text-center">
<a href="javascript:;" onClick={()=>this.selectAll('hiragana')}>All</a> &nbsp;&middot;&nbsp; <a href="javascript:;"
onClick={()=>this.selectNone('hiragana')}>None</a>
&nbsp;&middot;&nbsp; <a href="javascript:;" onClick={()=>this.selectAll('hiragana', true)}>All alternative</a>
&nbsp;&middot;&nbsp; <a href="javascript:;" onClick={()=>this.selectNone('hiragana', true)}>No alternative</a>
</div>
</div>
</div>
<div className="col-sm-6">
<div className="panel panel-default">
<div className="panel-heading">Katakana · カタカナ</div>
<div className="panel-body selection-areas">
{this.showGroupRows('katakana', this.state.showAlternatives.indexOf('katakana') >= 0, this.state.showSimilars.indexOf('katakana') >= 0)}
</div>
<div className="panel-footer text-center">
<a href="javascript:;" onClick={()=>this.selectAll('katakana')}>All</a> &nbsp;&middot;&nbsp; <a href="javascript:;"
onClick={()=>this.selectNone('katakana')}>None
</a>
&nbsp;&middot;&nbsp; <a href="javascript:;" onClick={()=>this.selectAll('katakana', true)}>All alternative</a>
&nbsp;&middot;&nbsp; <a href="javascript:;" onClick={()=>this.selectNone('katakana', true)}>No alternative</a>
</div>
</div>
</div>
<div className="col-sm-3 col-xs-12 pull-right">
<span className="pull-right lock">Lock to stage &nbsp;
{
this.props.isLocked &&
<input className="stage-choice" type="number" min="1" max="4" maxLength="1" size="1"
onChange={(e)=>this.props.lockStage(e.target.value, true)}
value={this.props.stage}
/>
}
<Switch onClick={()=>this.props.lockStage(1)} on={this.props.isLocked} /></span>
</div>
<div className="col-sm-offset-3 col-sm-6 col-xs-12 text-center">
{
this.state.errMsg != '' &&
<div className="error-message">{this.state.errMsg}</div>
}
<button ref={c => this.startRef = c} className="btn btn-danger startgame-button" onClick={() => this.startGame()}>Start the Quiz!</button>
</div>
<div className="down-arrow"
style={{display: this.state.startIsVisible ? 'none' : 'block'}}
onClick={(e) => this.scrollToStart(e)}
>
Start
</div>
</div>
</div>
);
}
} }
export default ChooseCharacters; export default ChooseCharacters;

View File

@ -1,143 +1,143 @@
.welcome { .welcome {
p:last-child { p:last-child {
margin-bottom: 0; margin-bottom: 0;
} }
h4 { h4 {
margin-top: 0; margin-top: 0;
} }
} }
.choose-characters .choose-characters
{ {
.panel-heading { .panel-heading {
font-weight: bold; font-weight: bold;
} }
.panel-heading span { .panel-heading span {
color: #aaa; color: #aaa;
} }
.panel-footer a { .panel-footer a {
text-decoration: none; text-decoration: none;
color: #337ab7; color: #337ab7;
} }
.choose-row { .choose-row {
font-size: 1em; font-size: 1em;
padding: 5px; padding: 5px;
} }
.choose-row:not(:last-child) { .choose-row:not(:last-child) {
border-bottom: 1px #eee solid; border-bottom: 1px #eee solid;
} }
.alt-row { .alt-row {
padding-left: 20px; padding-left: 20px;
background-color: #fafafa; background-color: #fafafa;
} }
.toggle-caret { .toggle-caret {
margin: 0 4px; margin: 0 4px;
} }
@media (min-width: 768px) { @media (min-width: 768px) {
.choose-row:hover {
background-color: #f4f4f4;
}
}
.choose-row:hover { .choose-row:hover {
cursor: pointer; background-color: #f4f4f4;
}
.glyphicon {
font-size: 0.9em;
}
.glyphicon-check {
color: green;
}
.glyphicon-check.half {
color: #ccc;
}
.glyphicon-unchecked {
color: #ccc;
}
.selection-areas {
padding: 7px;
}
.success-percent {
color: #ccc;
} }
}
.choose-row:hover {
cursor: pointer;
}
.glyphicon {
font-size: 0.9em;
}
.glyphicon-check {
color: green;
}
.glyphicon-check.half {
color: #ccc;
}
.glyphicon-unchecked {
color: #ccc;
}
.selection-areas {
padding: 7px;
}
.success-percent {
color: #ccc;
}
} }
.error-message { .error-message {
color: #d9534f; color: #d9534f;
padding-bottom: 10px; padding-bottom: 10px;
} }
.lock { .lock {
color: #888; color: #888;
margin-bottom: 10px; margin-bottom: 10px;
} }
.stage-choice { .stage-choice {
border: 1px solid #999; border: 1px solid #999;
padding-left: 5px; padding-left: 5px;
border-radius: 5px; border-radius: 5px;
margin-right: 5px; margin-right: 5px;
outline: none; outline: none;
} }
.switch { .switch {
border: 1px solid #ccc; border: 1px solid #ccc;
background: #e5e5e5; background: #e5e5e5;
width: 44px; width: 44px;
height: 19px; height: 19px;
border-radius: 13px; border-radius: 13px;
cursor: pointer; cursor: pointer;
display: block; display: block;
float: right; float: right;
margin-top: 1px; margin-top: 1px;
} }
.switch-toggle { .switch-toggle {
border: 1px solid #999; border: 1px solid #999;
box-shadow: 1px 1px 1px #ccc; box-shadow: 1px 1px 1px #ccc;
width: 19px; width: 19px;
height: 17px; height: 17px;
left: 0; left: 0;
border-radius: 12px; border-radius: 12px;
background: #eee; background: #eee;
position: relative; position: relative;
transition: left .2s ease-in-out; transition: left .2s ease-in-out;
} }
.switch.on { .switch.on {
border: 1px solid #4da94f; border: 1px solid #4da94f;
background: #5cb85c; background: #5cb85c;
} }
.switch.on .switch-toggle { .switch.on .switch-toggle {
left: 23px; left: 23px;
border: 1px solid #b8ffb2; border: 1px solid #b8ffb2;
background: #b8ffb2; background: #b8ffb2;
} }
.switch.disabled { .switch.disabled {
cursor: not-allowed; cursor: not-allowed;
} }
.down-arrow { .down-arrow {
user-select: none; user-select: none;
cursor: pointer; cursor: pointer;
border-radius: 4px 4px 0 0; border-radius: 4px 4px 0 0;
display: block; display: block;
position: fixed; position: fixed;
bottom: 20px; bottom: 20px;
right: 12px; right: 12px;
color: #fff; color: #fff;
background: #d9534f; background: #d9534f;
padding: 7px 0 2px; padding: 7px 0 2px;
width: 60px; width: 60px;
text-align: center; text-align: center;
} }
.down-arrow:after { .down-arrow:after {
content: ''; content: '';
display: block; display: block;
position: absolute; position: absolute;
left: 0; left: 0;
top: 100%; top: 100%;
width: 0; width: 0;
height: 0; height: 0;
border-top: 10px solid #d9534f; border-top: 10px solid #d9534f;
border-right: 30px solid transparent; border-right: 30px solid transparent;
border-bottom: 0 solid transparent; border-bottom: 0 solid transparent;
border-left: 30px solid transparent; border-left: 30px solid transparent;
} }

View File

@ -4,42 +4,40 @@ import ShowStage from './ShowStage';
import Question from './Question'; import Question from './Question';
class Game extends Component { class Game extends Component {
constructor(props) { state = { showScreen: '' }
super(props);
this.state = { componentWillMount() {
showScreen: '' this.setState({showScreen: 'stage'});
}
stageUp = () => {
this.props.stageUp();
this.setState({showScreen: 'stage'});
}
lockStage = stage => {
this.setState({showScreen: 'question'});
this.props.lockStage(stage);
}
showQuestion = () => {
this.setState({showScreen: 'question'})
}
render() {
return (
<div>
{
this.state.showScreen==='stage' &&
<ShowStage lockStage={this.lockStage} handleShowQuestion={this.showQuestion} handleEndGame={this.props.handleEndGame} stage={this.props.stage} />
} }
this.showQuestion = this.showQuestion.bind(this); {
this.stageUp = this.stageUp.bind(this); this.state.showScreen==='question' &&
this.lockStage = this.lockStage.bind(this); <Question isLocked={this.props.isLocked} handleStageUp={this.stageUp} stage={this.props.stage} decidedGroups={this.props.decidedGroups} />
} }
</div>
stageUp() { );
this.props.stageUp(); }
this.setState({showScreen: 'stage'});
}
lockStage(stage) {
this.setState({showScreen: 'question'});
this.props.lockStage(stage);
}
showQuestion() {
this.setState({showScreen: 'question'})
}
componentWillMount() {
this.setState({showScreen: 'stage'});
}
render() {
return (
<div>
{ this.state.showScreen==='stage' ? <ShowStage lockStage={this.lockStage} handleShowQuestion={this.showQuestion} handleEndGame={this.props.handleEndGame} stage={this.props.stage} /> : '' }
{ this.state.showScreen==='question' ? <Question isLocked={this.props.isLocked} handleStageUp={this.stageUp} stage={this.props.stage} decidedGroups={this.props.decidedGroups} /> : '' }
</div>
);
}
} }
export default Game; export default Game;

View File

@ -5,264 +5,272 @@ import { findRomajisAtKanaKey, removeFromArray, arrayContains, shuffle, cartesia
import './Question.scss'; import './Question.scss';
class Question extends Component { class Question extends Component {
constructor(props) { state = {
super(props); previousQuestion: [],
this.state = { previousAnswer: '',
previousQuestion: [], currentAnswer: '',
previousAnswer: '', currentQuestion: [],
currentAnswer: '', answerOptions: [],
currentQuestion: [], stageProgress: 0
answerOptions: [], }
stageProgress: 0 // this.setNewQuestion = this.setNewQuestion.bind(this);
// this.handleAnswer = this.handleAnswer.bind(this);
// this.handleAnswerChange = this.handleAnswerChange.bind(this);
// this.handleSubmit = this.handleSubmit.bind(this);
// }
getRandomKanas(amount, include, exclude) {
let randomizedKanas = this.askableKanaKeys.slice();
if(exclude && exclude.length > 0) {
// we're excluding previous question when deciding a new question
randomizedKanas = removeFromArray(exclude, randomizedKanas);
}
if(include && include.length > 0) {
// we arrive here when we're deciding answer options (included = currentQuestion)
// remove included kana
randomizedKanas = removeFromArray(include, randomizedKanas);
shuffle(randomizedKanas);
// cut the size to make looping quicker
randomizedKanas = randomizedKanas.slice(0,20);
// let's remove kanas that have the same answer as included
let searchFor = findRomajisAtKanaKey(include, kanaDictionary)[0];
randomizedKanas = randomizedKanas.filter(character => {
return searchFor!=findRomajisAtKanaKey(character, kanaDictionary)[0];
});
// now let's remove "duplicate" kanas (if two kanas have same answers)
let tempRandomizedKanas = randomizedKanas.slice();
randomizedKanas = randomizedKanas.filter(r => {
let dupeFound = false;
searchFor = findRomajisAtKanaKey(r, kanaDictionary)[0];
tempRandomizedKanas.shift();
tempRandomizedKanas.forEach(w => {
if(findRomajisAtKanaKey(w, kanaDictionary)[0]==searchFor)
dupeFound = true;
});
return !dupeFound;
});
// alright, let's cut the array and add included to the end
randomizedKanas = randomizedKanas.slice(0, amount-1); // -1 so we have room to add included
randomizedKanas.push(include);
shuffle(randomizedKanas);
}
else {
shuffle(randomizedKanas);
randomizedKanas = randomizedKanas.slice(0, amount);
}
return randomizedKanas;
}
setNewQuestion() {
if(this.props.stage!=4)
this.currentQuestion = this.getRandomKanas(1, false, this.previousQuestion);
else
this.currentQuestion = this.getRandomKanas(3, false, this.previousQuestion);
this.setState({currentQuestion: this.currentQuestion});
this.setAnswerOptions();
this.setAllowedAnswers();
// console.log(this.currentQuestion);
}
setAnswerOptions() {
this.answerOptions = this.getRandomKanas(3, this.currentQuestion[0], false);
this.setState({answerOptions: this.answerOptions});
// console.log(this.answerOptions);
}
setAllowedAnswers() {
// console.log(this.currentQuestion);
this.allowedAnswers = [];
if(this.props.stage==1 || this.props.stage==3)
this.allowedAnswers = findRomajisAtKanaKey(this.currentQuestion, kanaDictionary);
else if(this.props.stage==2)
this.allowedAnswers = this.currentQuestion;
else if(this.props.stage==4) {
let tempAllowedAnswers = [];
this.currentQuestion.forEach(key => {
tempAllowedAnswers.push(findRomajisAtKanaKey(key, kanaDictionary));
});
cartesianProduct(tempAllowedAnswers).forEach(answer => {
this.allowedAnswers.push(answer.join(''));
});
}
// console.log(this.allowedAnswers);
}
handleAnswer = answer => {
if(this.props.stage<=2) document.activeElement.blur(); // reset answer button's :active
this.previousQuestion = this.currentQuestion;
this.setState({previousQuestion: this.previousQuestion});
this.previousAnswer = answer;
this.setState({previousAnswer: this.previousAnswer});
this.previousAllowedAnswers = this.allowedAnswers;
if(this.isInAllowedAnswers(this.previousAnswer))
this.stageProgress = this.stageProgress+1;
else
this.stageProgress = this.stageProgress > 0 ? this.stageProgress - 1 : 0;
this.setState({stageProgress: this.stageProgress});
if(this.stageProgress >= quizSettings.stageLength[this.props.stage] && !this.props.isLocked) {
setTimeout(() => { this.props.handleStageUp() }, 300);
}
else
this.setNewQuestion();
}
initializeCharacters() {
this.askableKanas = {};
this.askableKanaKeys = [];
this.askableRomajis = [];
this.previousQuestion = '';
this.previousAnswer = '';
this.stageProgress = 0;
Object.keys(kanaDictionary).forEach(whichKana => {
// console.log(whichKana); // 'hiragana' or 'katakana'
Object.keys(kanaDictionary[whichKana]).forEach(groupName => {
// console.log(groupName); // 'h_group1', ...
// do we want to include this group?
if(arrayContains(groupName, this.props.decidedGroups)) {
// let's merge the group to our askableKanas
this.askableKanas = Object.assign(this.askableKanas, kanaDictionary[whichKana][groupName]['characters']);
Object.keys(kanaDictionary[whichKana][groupName]['characters']).forEach(key => {
// let's add all askable kana keys to array
this.askableKanaKeys.push(key);
this.askableRomajis.push(kanaDictionary[whichKana][groupName]['characters'][key][0]);
});
} }
this.setNewQuestion = this.setNewQuestion.bind(this); });
this.handleAnswer = this.handleAnswer.bind(this); });
this.handleAnswerChange = this.handleAnswerChange.bind(this); // console.log(this.askableKanas);
this.handleSubmit = this.handleSubmit.bind(this); }
}
getRandomKanas(amount, include, exclude) { getAnswerType() {
let randomizedKanas = this.askableKanaKeys.slice(); if(this.props.stage==2) return 'kana';
else return 'romaji';
}
if(exclude && exclude.length > 0) { getShowableQuestion() {
// we're excluding previous question when deciding a new question if(this.getAnswerType()=='kana')
randomizedKanas = removeFromArray(exclude, randomizedKanas); return findRomajisAtKanaKey(this.state.currentQuestion, kanaDictionary)[0];
} else return this.state.currentQuestion;
}
if(include && include.length > 0) {
// we arrive here when we're deciding answer options (included = currentQuestion)
// remove included kana
randomizedKanas = removeFromArray(include, randomizedKanas);
shuffle(randomizedKanas);
// cut the size to make looping quicker getPreviousResult() {
randomizedKanas = randomizedKanas.slice(0,20); let resultString='';
// console.log(this.previousAnswer);
if(this.previousQuestion=='')
resultString = <div className="previous-result none">Let's go! Which character is this?</div>
else {
let rightAnswer = (
this.props.stage==2 ?
findRomajisAtKanaKey(this.previousQuestion, kanaDictionary)[0]
: this.previousQuestion.join('')
)+' = '+ this.previousAllowedAnswers[0];
// let's remove kanas that have the same answer as included if(this.isInAllowedAnswers(this.previousAnswer))
let searchFor = findRomajisAtKanaKey(include, kanaDictionary)[0]; resultString = (
randomizedKanas = randomizedKanas.filter(function(character) { <div className="previous-result correct" title="Correct answer!">
return searchFor!=findRomajisAtKanaKey(character, kanaDictionary)[0]; <span className="pull-left glyphicon glyphicon-none"></span>{rightAnswer}<span className="pull-right glyphicon glyphicon-ok"></span>
}, this); </div>
);
// now let's remove "duplicate" kanas (if two kanas have same answers) else
let tempRandomizedKanas = randomizedKanas.slice(); resultString = (
randomizedKanas = randomizedKanas.filter(function(r) { <div className="previous-result wrong" title="Wrong answer!">
let dupeFound = false; <span className="pull-left glyphicon glyphicon-none"></span>{rightAnswer}<span className="pull-right glyphicon glyphicon-remove"></span>
searchFor = findRomajisAtKanaKey(r, kanaDictionary)[0]; </div>
tempRandomizedKanas.shift();
tempRandomizedKanas.map(function(w) {
if(findRomajisAtKanaKey(w, kanaDictionary)[0]==searchFor)
dupeFound = true;
}, this);
return !dupeFound;
}, this);
// alright, let's cut the array and add included to the end
randomizedKanas = randomizedKanas.slice(0, amount-1); // -1 so we have room to add included
randomizedKanas.push(include);
shuffle(randomizedKanas);
}
else {
shuffle(randomizedKanas);
randomizedKanas = randomizedKanas.slice(0, amount);
}
return randomizedKanas;
}
setNewQuestion() {
if(this.props.stage!=4)
this.currentQuestion = this.getRandomKanas(1, false, this.previousQuestion);
else
this.currentQuestion = this.getRandomKanas(3, false, this.previousQuestion);
this.setState({currentQuestion: this.currentQuestion});
this.setAnswerOptions();
this.setAllowedAnswers();
// console.log(this.currentQuestion);
}
setAnswerOptions() {
this.answerOptions = this.getRandomKanas(3, this.currentQuestion[0], false);
this.setState({answerOptions: this.answerOptions});
// console.log(this.answerOptions);
}
setAllowedAnswers() {
// console.log(this.currentQuestion);
this.allowedAnswers = [];
if(this.props.stage==1 || this.props.stage==3)
this.allowedAnswers = findRomajisAtKanaKey(this.currentQuestion, kanaDictionary);
else if(this.props.stage==2)
this.allowedAnswers = this.currentQuestion;
else if(this.props.stage==4) {
let tempAllowedAnswers = [];
this.currentQuestion.map(function(key, idx) {
tempAllowedAnswers.push(findRomajisAtKanaKey(key, kanaDictionary));
}, this);
cartesianProduct(tempAllowedAnswers).map(function(answer) {
this.allowedAnswers.push(answer.join(''));
}, this);
}
// console.log(this.allowedAnswers);
}
handleAnswer(answer) {
if(this.props.stage<=2) document.activeElement.blur(); // reset answer button's :active
this.previousQuestion = this.currentQuestion;
this.setState({previousQuestion: this.previousQuestion});
this.previousAnswer = answer;
this.setState({previousAnswer: this.previousAnswer});
this.previousAllowedAnswers = this.allowedAnswers;
if(this.isInAllowedAnswers(this.previousAnswer))
this.stageProgress = this.stageProgress+1;
else
this.stageProgress = this.stageProgress > 0 ? this.stageProgress - 1 : 0;
this.setState({stageProgress: this.stageProgress});
if(this.stageProgress >= quizSettings.stageLength[this.props.stage] &&
!this.props.isLocked) {
let that = this;
setTimeout(function() { that.props.handleStageUp(); }, 300);
}
else
this.setNewQuestion();
}
initializeCharacters() {
this.askableKanas = {};
this.askableKanaKeys = [];
this.askableRomajis = [];
this.previousQuestion = '';
this.previousAnswer = '';
this.stageProgress = 0;
Object.keys(kanaDictionary).map(function(whichKana) {
// console.log(whichKana); // 'hiragana' or 'katakana'
Object.keys(kanaDictionary[whichKana]).map(function(groupName) {
// console.log(groupName); // 'h_group1', ...
// do we want to include this group?
if(arrayContains(groupName, this.props.decidedGroups)) {
// let's merge the group to our askableKanas
this.askableKanas = Object.assign(this.askableKanas, kanaDictionary[whichKana][groupName]['characters']);
Object.keys(kanaDictionary[whichKana][groupName]['characters']).map(function(key) {
// let's add all askable kana keys to array
this.askableKanaKeys.push(key);
this.askableRomajis.push(kanaDictionary[whichKana][groupName]['characters'][key][0]);
}, this);
}
}, this);
}, this);
// console.log(this.askableKanas);
}
getAnswerType() {
if(this.props.stage==2) return 'kana';
else return 'romaji';
}
getShowableQuestion() {
if(this.getAnswerType()=='kana')
return findRomajisAtKanaKey(this.state.currentQuestion, kanaDictionary)[0];
else return this.state.currentQuestion;
}
getPreviousResult() {
let resultString='';
// console.log(this.previousAnswer);
if(this.previousQuestion=='')
resultString = <div className="previous-result none">Let's go! Which character is this?</div>
else {
let rightAnswer = (this.props.stage==2?findRomajisAtKanaKey(this.previousQuestion, kanaDictionary)[0]:this.previousQuestion.join(''))+' = '+
this.previousAllowedAnswers[0];
if(this.isInAllowedAnswers(this.previousAnswer))
resultString = <div className="previous-result correct" title="Correct answer!"><span className="pull-left glyphicon glyphicon-none"></span>{rightAnswer}<span className="pull-right glyphicon glyphicon-ok"></span></div>
else
resultString = <div className="previous-result wrong" title="Wrong answer!"><span className="pull-left glyphicon glyphicon-none"></span>{rightAnswer}<span className="pull-right glyphicon glyphicon-remove"></span></div>
}
return resultString;
}
isInAllowedAnswers(previousAnswer) {
// console.log(previousAnswer);
// console.log(this.allowedAnswers);
if(arrayContains(previousAnswer, this.previousAllowedAnswers))
return true;
else return false;
}
handleAnswerChange(e) {
this.setState({currentAnswer: e.target.value.replace(/\s+/g, '')});
}
handleSubmit(e) {
e.preventDefault();
if(this.state.currentAnswer!='') {
this.handleAnswer(this.state.currentAnswer.toLowerCase());
this.setState({currentAnswer: ''});
}
}
componentWillMount() {
this.initializeCharacters();
}
componentDidMount() {
this.setNewQuestion();
}
render() {
let btnClass = "btn btn-default answer-button";
if ('ontouchstart' in window)
btnClass += " no-hover"; // disables hover effect on touch screens
let stageProgressPercentage = Math.round((this.state.stageProgress/quizSettings.stageLength[this.props.stage])*100)+'%';
let stageProgressPercentageStyle = { width: stageProgressPercentage }
return (
<div className="text-center question col-xs-12">
{this.getPreviousResult()}
<div className="big-character">{this.getShowableQuestion()}</div>
<div className="answer-container">
{this.props.stage<3?this.state.answerOptions.map(function(answer, idx) {
return <AnswerButton answer={answer}
className={btnClass}
key={idx}
answertype={this.getAnswerType()}
handleAnswer={this.handleAnswer} />
}, this):
<div className="answer-form-container">
<form onSubmit={this.handleSubmit}>
<input autoFocus className="answer-input" type="text" value={this.state.currentAnswer} onChange={this.handleAnswerChange} />
</form>
</div>
}
</div>
<div className="progress">
<div className="progress-bar progress-bar-info"
role="progressbar"
aria-valuenow={this.state.stageProgress}
aria-valuemin="0"
aria-valuemax={quizSettings.stageLength[this.props.stage]}
style={stageProgressPercentageStyle}
>
<span>Stage {this.props.stage} {this.props.isLocked?' (Locked)':''}</span>
</div>
</div>
</div>
); );
} }
return resultString;
}
isInAllowedAnswers(previousAnswer) {
// console.log(previousAnswer);
// console.log(this.allowedAnswers);
if(arrayContains(previousAnswer, this.previousAllowedAnswers))
return true;
else return false;
}
handleAnswerChange = e => {
this.setState({currentAnswer: e.target.value.replace(/\s+/g, '')});
}
handleSubmit = e => {
e.preventDefault();
if(this.state.currentAnswer!='') {
this.handleAnswer(this.state.currentAnswer.toLowerCase());
this.setState({currentAnswer: ''});
}
}
componentWillMount() {
this.initializeCharacters();
}
componentDidMount() {
this.setNewQuestion();
}
render() {
let btnClass = "btn btn-default answer-button";
if ('ontouchstart' in window)
btnClass += " no-hover"; // disables hover effect on touch screens
let stageProgressPercentage = Math.round((this.state.stageProgress/quizSettings.stageLength[this.props.stage])*100)+'%';
let stageProgressPercentageStyle = { width: stageProgressPercentage }
return (
<div className="text-center question col-xs-12">
{this.getPreviousResult()}
<div className="big-character">{this.getShowableQuestion()}</div>
<div className="answer-container">
{this.props.stage<3?this.state.answerOptions.map(function(answer, idx) {
return <AnswerButton answer={answer}
className={btnClass}
key={idx}
answertype={this.getAnswerType()}
handleAnswer={this.handleAnswer} />
}, this):
<div className="answer-form-container">
<form onSubmit={this.handleSubmit}>
<input autoFocus className="answer-input" type="text" value={this.state.currentAnswer} onChange={this.handleAnswerChange} />
</form>
</div>
}
</div>
<div className="progress">
<div className="progress-bar progress-bar-info"
role="progressbar"
aria-valuenow={this.state.stageProgress}
aria-valuemin="0"
aria-valuemax={quizSettings.stageLength[this.props.stage]}
style={stageProgressPercentageStyle}
>
<span>Stage {this.props.stage} {this.props.isLocked?' (Locked)':''}</span>
</div>
</div>
</div>
);
}
} }
class AnswerButton extends Component { class AnswerButton extends Component {
getShowableAnswer() { getShowableAnswer() {
if(this.props.answertype=='romaji') if(this.props.answertype=='romaji')
return findRomajisAtKanaKey(this.props.answer, kanaDictionary)[0]; return findRomajisAtKanaKey(this.props.answer, kanaDictionary)[0];
else return this.props.answer; else return this.props.answer;
} }
render() { render() {
return ( return (
<button className={this.props.className} onClick={()=>this.props.handleAnswer(this.getShowableAnswer())}>{this.getShowableAnswer()}</button> <button className={this.props.className} onClick={()=>this.props.handleAnswer(this.getShowableAnswer())}>{this.getShowableAnswer()}</button>
); );
} }
} }
export default Question; export default Question;

View File

@ -1,87 +1,87 @@
.question { .question {
.progress { .progress {
position: relative; position: relative;
background-color: #ddd; background-color: #ddd;
height: 30px; height: 30px;
@media (max-width: 768px) { @media (max-width: 768px) {
max-width: 360px; max-width: 360px;
margin: 0 auto; margin: 0 auto;
}
} }
.progress span { }
position: absolute; .progress span {
top: 5px; position: absolute;
color: #444; top: 5px;
display: block; color: #444;
width: 100%; display: block;
width: 100%;
}
.previous-result{
max-width: 360px;
padding: 8px;
margin: 30px auto 28px;
border-radius: 3px;
@media (max-width: 768px) {
margin: 0px auto 0px;
} }
.previous-result{ }
max-width: 360px; .size-up {
padding: 8px; font-size: 1.1em;
margin: 30px auto 28px; }
border-radius: 3px; .none {
@media (max-width: 768px) { background-color: #aaa;
margin: 0px auto 0px; color: #f5f5f5;
} }
.correct {
color: #f5f5f5;
background-color: #5cb85c;
}
.wrong {
color: #f5f5f5;
background-color: #d9534f;
}
.previous-result-none {
max-width: 360px;
padding: 6px;
margin: 30px auto;
color: #f5f5f5;
}
.big-character {
font-size: 5em;
}
.answer-container {
max-width: 360px;
margin: 0 auto;
display: flex;
justify-content: space-between;
}
.answer-form-container {
max-width: 100px;
margin: 0 auto;
}
.answer-button {
min-width: 90px;
font-size: 2em;
margin: 30px 0 60px;
@media (max-width: 768px) {
margin: 0px 0 15px;
} }
.size-up { }
font-size: 1.1em; .answer-input, .answer-input:focus {
outline: none;
width: 110px;
text-align: center;
font-size: 2em;
margin: 25px 0 60px;
background: none;
border: none;
border-bottom: solid 1px #aaa;
@media (max-width: 768px) {
margin: 0px 0 25px;
} }
.none { }
background-color: #aaa; .no-hover {
color: #f5f5f5; /* disables hover effect on touch screens */
} background-color: #fff;
.correct { border-color: #ccc;
color: #f5f5f5; }
background-color: #5cb85c; }
}
.wrong {
color: #f5f5f5;
background-color: #d9534f;
}
.previous-result-none {
max-width: 360px;
padding: 6px;
margin: 30px auto;
color: #f5f5f5;
}
.big-character {
font-size: 5em;
}
.answer-container {
max-width: 360px;
margin: 0 auto;
display: flex;
justify-content: space-between;
}
.answer-form-container {
max-width: 100px;
margin: 0 auto;
}
.answer-button {
min-width: 90px;
font-size: 2em;
margin: 30px 0 60px;
@media (max-width: 768px) {
margin: 0px 0 15px;
}
}
.answer-input, .answer-input:focus {
outline: none;
width: 110px;
text-align: center;
font-size: 2em;
margin: 25px 0 60px;
background: none;
border: none;
border-bottom: solid 1px #aaa;
@media (max-width: 768px) {
margin: 0px 0 25px;
}
}
.no-hover {
/* disables hover effect on touch screens */
background-color: #fff;
border-color: #ccc;
}
}

View File

@ -4,66 +4,63 @@ import ChooseCharacters from '../ChooseCharacters/ChooseCharacters';
import Game from '../Game/Game'; import Game from '../Game/Game';
class GameContainer extends Component { class GameContainer extends Component {
constructor(props) { state = {
super(props); stage:1,
this.startGame = this.startGame.bind(this); isLocked: false,
this.state = { decidedGroups: JSON.parse(localStorage.getItem('decidedGroups') || null) || []
stage:1, }
isLocked: false,
decidedGroups: ['h_group1']
}
this.stageUp = this.stageUp.bind(this);
this.lockStage = this.lockStage.bind(this);
}
startGame(decidedGroups) { componentWillReceiveProps() {
if(parseInt(this.state.stage)<1 || isNaN(parseInt(this.state.stage))) if(!this.state.isLocked)
this.setState({stage: 1}); this.setState({stage: 1});
else if(parseInt(this.state.stage)>4) }
this.setState({stage: 4});
this.setState({decidedGroups: decidedGroups}); startGame = decidedGroups => {
this.props.handleStartGame(); if(parseInt(this.state.stage)<1 || isNaN(parseInt(this.state.stage)))
} this.setState({stage: 1});
else if(parseInt(this.state.stage)>4)
this.setState({stage: 4});
stageUp() { this.setState({decidedGroups: decidedGroups});
this.setState({stage: this.state.stage+1}); localStorage.setItem('decidedGroups', JSON.stringify(decidedGroups));
} this.props.handleStartGame();
}
lockStage(stage, forceLock) { stageUp = () => {
// if(stage<1 || stage>4) stage=1; // don't use this to allow backspace this.setState({stage: this.state.stage+1});
if(forceLock) }
this.setState({stage: stage, isLocked: true});
else
this.setState({stage: stage, isLocked: !this.state.isLocked});
}
componentWillReceiveProps() { lockStage = (stage, forceLock) => {
if(!this.state.isLocked) // if(stage<1 || stage>4) stage=1; // don't use this to allow backspace
this.setState({stage: 1}); if(forceLock)
} this.setState({stage: stage, isLocked: true});
else
this.setState({stage: stage, isLocked: !this.state.isLocked});
}
render() { render() {
return ( return (
<div> <div>
{ this.props.gameState==='chooseCharacters' ? { this.props.gameState==='chooseCharacters' &&
<ChooseCharacters selectedGroups={this.state.decidedGroups} <ChooseCharacters selectedGroups={this.state.decidedGroups}
handleStartGame={this.startGame} handleStartGame={this.startGame}
stage={this.state.stage} stage={this.state.stage}
isLocked={this.state.isLocked} isLocked={this.state.isLocked}
lockStage={this.lockStage} lockStage={this.lockStage}
/> : '' } />
{ this.props.gameState==='game' ? }
<Game decidedGroups={this.state.decidedGroups} { this.props.gameState==='game' &&
handleEndGame={this.props.handleEndGame} <Game decidedGroups={this.state.decidedGroups}
stageUp={this.stageUp} handleEndGame={this.props.handleEndGame}
stage={this.state.stage} stageUp={this.stageUp}
isLocked={this.state.isLocked} stage={this.state.stage}
lockStage={this.lockStage} isLocked={this.state.isLocked}
/> : '' } lockStage={this.lockStage}
</div> />
) }
} </div>
)
}
} }
export default GameContainer; export default GameContainer;

View File

@ -2,28 +2,27 @@ import React, { Component } from 'react';
import './Navbar.scss'; import './Navbar.scss';
class Navbar extends Component { class Navbar extends Component {
render() { render() {
let leftLink; return (
switch(this.props.gameState) { <nav className="navbar navbar-inverse navbar-fixed-top" role="navigation">
case 'chooseCharacters': <div className="container">
default: <div id="navbar">
leftLink = <li id="nav-kanaquiz"><p className="nav navbar-text">Kana Quiz <span>2</span></p></li> <ul className="nav navbar-nav">
break; {
case 'game': this.props.gameState == 'game' ? (
leftLink = <li id="nav-choosecharacters"><a href="javascript:;" onClick={this.props.handleEndGame}><span className="glyphicon glyphicon-small glyphicon-arrow-left"></span> Back to menu</a></li> <li id="nav-choosecharacters">
} <a href="javascript:;" onClick={this.props.handleEndGame}>
return ( <span className="glyphicon glyphicon-small glyphicon-arrow-left"></span> Back to menu
<nav className="navbar navbar-inverse navbar-fixed-top" role="navigation"> </a>
<div className="container"> </li>
<div id="navbar"> ) : <li id="nav-kanaquiz"><p className="nav navbar-text">Kana Pro</p></li>
<ul className="nav navbar-nav"> }
{leftLink} </ul>
</ul> </div>
</div> </div>
</div> </nav>
</nav> )
) }
}
} }
export default Navbar; export default Navbar;

View File

@ -1,15 +1,15 @@
#navbar ul li { #navbar ul li {
font-size: 1.2em; font-size: 1.2em;
@media (max-width: 768px) { @media (max-width: 768px) {
font-size: 1.0em; font-size: 1.0em;
} }
.glyphicon { .glyphicon {
font-size: 0.9em; font-size: 0.9em;
color: #d9534f; color: #d9534f;
} }
} }
.navbar-inverse { .navbar-inverse {
background-color: #333; background-color: #333;
} }
.hidden-nano { .hidden-nano {
@media (max-width: 390px) { @media (max-width: 390px) {
@ -88,7 +88,7 @@
border-width: 0 1px 1px; border-width: 0 1px 1px;
border-radius: 0 0 4px 4px; border-radius: 0 0 4px 4px;
-webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, .175); -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, .175);
box-shadow: 0 6px 12px rgba(0, 0, 0, .175); box-shadow: 0 6px 12px rgba(0, 0, 0, .175);
} }
.navbar-default .navbar-nav .open .dropdown-menu > li > a { .navbar-default .navbar-nav .open .dropdown-menu > li > a {
color: #333; color: #333;
@ -106,4 +106,4 @@
.navbar .navbar-nav .open .dropdown-menu > .disabled > a:focus { .navbar .navbar-nav .open .dropdown-menu > .disabled > a:focus {
color: #999 !important; color: #999 !important;
background-color: transparent !important; background-color: transparent !important;
} }

View File

@ -1,13 +1,13 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<title>Kana Quiz 2: Learn hiragana & katakana fast and easy</title> <title>Kana Pro: Learn hiragana & katakana fast and easy</title>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="Description" content="Application for studying hiragana & katakana characters."> <meta name="Description" content="Application for studying hiragana & katakana characters.">
</head> </head>
<body> <body>
<div class="app"></div> <div id="app"></div>
</body> </body>
</html> </html>

View File

@ -1,7 +1,10 @@
import React from 'react'; import React from 'react';
import { render } from 'react-dom'; import ReactDOM from 'react-dom';
import Bootstrap from './assets/stylesheets/bootstrap.min.css'; import Bootstrap from './assets/stylesheets/bootstrap.min.css';
import App from './components/App/App'; import App from './components/App/App';
let element = React.createElement(App, {}); let appEl = document.getElementById('app');
render(element, document.querySelector('.app')); if(!appEl) // in case of old index.html in cache
appEl = document.querySelector('.app');
ReactDOM.render(<App />, appEl);