1
0
Fork 0
mirror of https://github.com/Alamantus/Lexiconga.git synced 2025-06-22 09:06:39 +02:00

Merge branch 'dev'

This commit is contained in:
Robbie Antenesse 2018-03-10 11:23:52 -07:00
commit ee710988d5
17 changed files with 398 additions and 97 deletions

View file

@ -45,6 +45,7 @@
"dependencies": { "dependencies": {
"babel-polyfill": "^6.26.0", "babel-polyfill": "^6.26.0",
"bulma": "^0.6.2", "bulma": "^0.6.2",
"bulma-checkradio": "^1.0.1",
"dexie": "^2.0.1", "dexie": "^2.0.1",
"font-awesome": "^4.7.0", "font-awesome": "^4.7.0",
"inferno": "^4.0.4", "inferno": "^4.0.4",

View file

@ -24,7 +24,10 @@ class User {
} }
} else if (password_verify($password, $user['password'])) { } else if (password_verify($password, $user['password'])) {
$this->db->execute('UPDATE users SET last_login=' . time() . ' WHERE id=' . $user['id']); $this->db->execute('UPDATE users SET last_login=' . time() . ' WHERE id=' . $user['id']);
return $this->generateUserToken($user['id'], $user['current_dictionary']); return array(
'token' => $this->generateUserToken($user['id'], $user['current_dictionary']),
'user' => $this->getUserData($user['id']),
);
} }
} }
return false; return false;
@ -60,13 +63,57 @@ VALUES (?, ?, ?, ?, ?, '. time() .')';
$new_dictionary = $this->dictionary->create($new_user_id); $new_dictionary = $this->dictionary->create($new_user_id);
if ($new_dictionary !== false) { if ($new_dictionary !== false) {
return $this->generateUserToken($new_user_id, $new_dictionary); return array(
'token' => $this->generateUserToken($new_user_id, $new_dictionary),
'user' => $this->getUserData($new_user_id),
);
} }
} }
return false; return false;
} }
public function setUserData ($token, $user_data) {
$token_data = $this->token->decode($token);
if ($token_data !== false) {
$query = 'UPDATE users SET email=?, public_name=?, username=?, allow_email=?, use_ipa=? WHERE id=?';
$properties = array(
$user_data['email'],
$user_data['publicName'],
$user_data['username'],
$user_data['allowEmail'],
$user_data['useIPAPronunciation'],
$user_id,
);
$update_success = $this->db->execute($query, $properties);
if ($update_success) {
return array(
'token' => $token,
'userData' => $user_data,
);
}
}
return false;
}
public function getUserData ($user_id) {
$query = 'SELECT * FROM users WHERE id=?';
$stmt = $this->db->query($query, array($user_id));
$user = $stmt->fetch();
if ($stmt && $user) {
return array(
'email' => $user['email'],
'username' => $user['username'],
'publicName' => $user['public_name'],
'allowEmails' => $user['allow_email'] == 1 ? true : false,
'useIPAPronunciation' => $user['use_ipa'] == 1 ? true : false,
);
}
return false;
}
public function createNewDictionary ($token) { public function createNewDictionary ($token) {
$user_data = $this->token->decode($token); $user_data = $this->token->decode($token);
if ($user_data !== false) { if ($user_data !== false) {

View file

@ -12,10 +12,10 @@ switch ($action) {
case 'login': { case 'login': {
if (isset($request['email']) && isset($request['password'])) { if (isset($request['email']) && isset($request['password'])) {
$user = new User(); $user = new User();
$token = $user->logIn($request['email'], $request['password']); $user_data = $user->logIn($request['email'], $request['password']);
if ($token !== false) { if ($user_data !== false) {
return Response::json(array( return Response::json(array(
'data' => $token, 'data' => $user_data,
'error' => false, 'error' => false,
), 200); ), 200);
} }
@ -33,10 +33,10 @@ switch ($action) {
if (isset($request['email']) && isset($request['password'])) { if (isset($request['email']) && isset($request['password'])) {
$user = new User(); $user = new User();
if (!$user->emailExists($request['email'])) { if (!$user->emailExists($request['email'])) {
$token = $user->create($request['email'], $request['password'], $request['userData']); $user_data = $user->create($request['email'], $request['password'], $request['userData']);
if ($token !== false) { if ($user_data !== false) {
return Response::json(array( return Response::json(array(
'data' => $token, 'data' => $user_data,
'error' => false, 'error' => false,
), 201); ), 201);
} }
@ -103,6 +103,26 @@ switch ($action) {
'error' => true, 'error' => true,
), 403); ), 403);
} }
case 'set-user-data': {
if ($token !== false && isset($request['userData'])) {
$user = new User();
$updated_user = $user->setUserData($token, $request['userData']);
if ($updated_user !== false) {
return Response::json(array(
'data' => $updated_user,
'error' => false,
), 200);
}
return Response::json(array(
'data' => 'Could not set user data: missing data',
'error' => true,
), 400);
}
return Response::json(array(
'data' => 'Could not get dictionaries: no token provided',
'error' => true,
), 403);
}
case 'create-new-dictionary': { case 'create-new-dictionary': {
if ($token !== false) { if ($token !== false) {
$user = new User(); $user = new User();

View file

@ -16,8 +16,10 @@ export class MainDisplay extends Component {
PropTypes.checkPropTypes({ PropTypes.checkPropTypes({
dictionaryInfo: PropTypes.object.isRequired, dictionaryInfo: PropTypes.object.isRequired,
isLoadingWords: PropTypes.bool,
wordsToDisplay: PropTypes.array.isRequired, wordsToDisplay: PropTypes.array.isRequired,
wordsAreFiltered: PropTypes.bool, wordsAreFiltered: PropTypes.bool,
wordsInCurrentList: PropTypes.number,
currentPage: PropTypes.number, currentPage: PropTypes.number,
itemsPerPage: PropTypes.number, itemsPerPage: PropTypes.number,
stats: PropTypes.object.isRequired, stats: PropTypes.object.isRequired,
@ -55,8 +57,10 @@ export class MainDisplay extends Component {
render () { render () {
const { const {
dictionaryInfo, dictionaryInfo,
isLoadingWords,
wordsToDisplay, wordsToDisplay,
wordsAreFiltered, wordsAreFiltered,
wordsInCurrentList,
currentPage, currentPage,
itemsPerPage, itemsPerPage,
stats, stats,
@ -105,7 +109,16 @@ export class MainDisplay extends Component {
</div> </div>
)} )}
<Pagination
currentPage={ currentPage }
itemsPerPage={ itemsPerPage }
stats={ stats }
setPage={ setPage }
wordsInCurrentList={ wordsInCurrentList }
isTop />
<WordsList <WordsList
isLoadingWords={ isLoadingWords }
words={ wordsToDisplay } words={ wordsToDisplay }
adsEveryXWords={ 10 } adsEveryXWords={ 10 }
updateDisplay={ updateDisplay } /> updateDisplay={ updateDisplay } />
@ -114,7 +127,8 @@ export class MainDisplay extends Component {
currentPage={currentPage} currentPage={currentPage}
itemsPerPage={itemsPerPage} itemsPerPage={itemsPerPage}
stats={stats} stats={stats}
setPage={ setPage } /> setPage={setPage}
wordsInCurrentList={wordsInCurrentList} />
</RightColumn> </RightColumn>
</div> </div>

View file

@ -13,6 +13,7 @@ export class WordsList extends Component {
super(props); super(props);
PropTypes.checkPropTypes({ PropTypes.checkPropTypes({
isLoadingWords: PropTypes.bool,
adsEveryXWords: PropTypes.number, adsEveryXWords: PropTypes.number,
words: PropTypes.array, words: PropTypes.array,
updateDisplay: PropTypes.func.isRequired, updateDisplay: PropTypes.func.isRequired,
@ -22,6 +23,12 @@ export class WordsList extends Component {
render () { render () {
const adsEveryXWords = this.props.adsEveryXWords || 10; const adsEveryXWords = this.props.adsEveryXWords || 10;
if (this.props.isLoadingWords) {
return <div className="content has-text-centered">
<div className="loader" style="display: inline-block; width: 60px; height: 60px;" />
</div>;
}
return ( return (
<div className='box'> <div className='box'>

View file

@ -0,0 +1,94 @@
import Inferno from 'inferno';
import { Component } from 'inferno';
import PropTypes from 'prop-types';
export class MyAccount extends Component {
constructor(props) {
super(props);
PropTypes.checkPropTypes({
email: PropTypes.string.isRequired,
username: PropTypes.string.isRequired,
publicName: PropTypes.string.isRequired,
allowEmails: PropTypes.bool.isRequired,
useIPAPronunciation: PropTypes.bool.isRequired,
userDictionaries: PropTypes.array.isRequired,
updateUserData: PropTypes.func,
changeDictionary: PropTypes.func,
}, props, 'prop', 'LoginForm');
this.state = {
email: this.props.email,
username: this.props.username,
publicName: this.props.publicName,
allowEmails: this.props.allowEmails,
useIPAPronunciation: this.props.useIPAPronunciation,
userDictionaries: this.props.userDictionaries,
};
}
render() {
return (
<div>
<div className='columns'>
<div className='column'>
<h2 className='title'>Account Details</h2>
<div className='control'>
<strong>Email:</strong> <span>{this.state.email}</span>
</div>
<div className='control'>
<strong>Username:</strong> <span>{this.state.username}</span>
</div>
<div className='field'>
<label className='label'>
<span>Public Name:</span>
</label>
<div className='control'>
<input className='input' type='text' value={this.state.publicName}
onInput={(event) => {this.setState({publicName: event.target.value})}} />
</div>
</div>
<div className='field'>
<div className='control'>
<input className='is-checkradio is-rtl' type='checkbox' id='allowEmails'
checked={this.state.allowEmails ? 'checked' : false}
onChange={(event) => { this.setState({ allowEmails: !this.state.allowEmails }) }} />
<label className='label is-unselectable' htmlFor='allowEmails'>
Allow Emails
</label>
</div>
</div>
<div className='field'>
<input className='is-checkradio is-rtl' type='checkbox' id='useIPAPronunciation'
checked={this.state.useIPAPronunciation ? 'checked' : false}
onChange={(event) => { this.setState({ useIPAPronunciation: !this.state.useIPAPronunciation }) }} />
<label className='label is-unselectable' htmlFor='useIPAPronunciation'>
Use IPA in Pronunciation Fields
</label>
</div>
</div>
<div className='column'>
<h2 className='title'>Account Actions</h2>
<div className='field'>
<label className='label is-unselectable'>
<span>Change Dictionary</span>
</label>
<div className='control'>
<div className='select'>
<select>
{this.props.userDictionaries.map(item => {
return <option value={item.id}>{item.name}</option>;
})}
</select>
</div>
</div>
</div>
</div>
</div>
</div>
);
}
}

View file

@ -6,9 +6,18 @@ import store from 'store';
import { Modal } from '../../structure/Modal'; import { Modal } from '../../structure/Modal';
import { LoginForm } from './LoginForm'; import { LoginForm } from './LoginForm';
import { MyAccount } from './MyAccount';
import { request } from '../../../Helpers'; import { request } from '../../../Helpers';
const defaultUserData = {
email: '',
username: '',
publicName: '',
allowEmails: true,
useIPAPronunciation: true,
};
export class AccountManager extends Component { export class AccountManager extends Component {
constructor (props) { constructor (props) {
super(props); super(props);
@ -17,9 +26,21 @@ export class AccountManager extends Component {
updater: PropTypes.object.isRequired, updater: PropTypes.object.isRequired,
}, props, 'prop', 'AccountManager'); }, props, 'prop', 'AccountManager');
const userData = store.get('LexicongaUserData');
this.state = { this.state = {
isLoggedIn: false, isLoggedIn: false,
userData: {
email: userData ? userData.email : defaultUserData.email,
username: userData ? userData.username : defaultUserData.username,
publicName: userData ? userData.publicName : defaultUserData.publicName,
allowEmails: userData ? userData.allowEmails : defaultUserData.allowEmails,
useIPAPronunciation: userData ? userData.useIPAPronunciation : defaultUserData.useIPAPronunciation,
},
userDictionaries: [],
}; };
this.getDictionaryNames();
} }
logIn (email, password) { logIn (email, password) {
@ -28,7 +49,12 @@ export class AccountManager extends Component {
logOut () { logOut () {
store.remove('LexicongaToken'); store.remove('LexicongaToken');
this.setState({ isLoggedIn: false }); store.remove('LexicongaUserData');
this.setState({
isLoggedIn: false,
userData: Object.assign({}, defaultUserData),
userDictionaries: [],
});
} }
signUp (email, password, userData) { signUp (email, password, userData) {
@ -44,23 +70,69 @@ export class AccountManager extends Component {
if (error) { if (error) {
console.error(data); console.error(data);
} else { } else {
store.set('LexicongaToken', data); store.set('LexicongaToken', data.token);
this.setState({ isLoggedIn: true }, () => { store.set('LexicongaUserData', data.user);
this.setState({
isLoggedIn: true,
userData: data.user,
}, () => {
this.getDictionaryNames();
this.props.updater.sync(); this.props.updater.sync();
}); });
} }
} }
updateUserData (newUserData) {
const token = store.get('LexicongaToken');
if (token) {
store.set('LexicongaUserData', newUserData);
this.setState({ userData: newUserData }, () => {
request('set-user-data', { token, userData: newUserData }, (response) => {
const {data, error} = response;
if (error) {
console.error(data);
} else {
console.log(data);
}
})
});
}
}
getDictionaryNames () {
const token = store.get('LexicongaToken');
if (token) {
return request('get-all-dictionary-names', { token }, (response) => {
const {data, error} = response;
if (error) {
console.error(data);
} else {
this.setState({ userDictionaries: data });
}
});
}
}
render () { render () {
const token = store.get('LexicongaToken'); const token = store.get('LexicongaToken');
if (token) { if (token) {
const { userData } = this.state;
return ( return (
<div> <div>
<Modal buttonText='Account' title='My Account'> <Modal buttonText='Account' title='My Account' onShow={ this.getDictionaryNames.bind(this) }>
<div className='content has-text-left'> <MyAccount
<p>Hello My Account!</p> email={ userData.email }
</div> username={ userData.username }
publicName={ userData.publicName }
allowEmails={ userData.allowEmails }
useIPAPronunciation={ userData.useIPAPronunciation }
userDictionaries={ this.state.userDictionaries }
updateUserData={ this.updateUserData.bind(this) }
changeDictionary={ () => {} } />
</Modal> </Modal>
<a className='button' onClick={this.logOut.bind(this)}> <a className='button' onClick={this.logOut.bind(this)}>
Log Out Log Out

View file

@ -30,17 +30,16 @@ export const EditSettingsForm = (props) => {
<div className='field'> <div className='field'>
<div className='control'> <div className='control'>
<label className='checkbox'> <input className='is-checkradio is-rtl' type='checkbox' id='allowDuplicates'
defaultChecked={ allowDuplicates }
onChange={ (event) => {
editDictionaryModal.setState({
allowDuplicates: event.target.checked,
hasChanged: event.target.checked != editDictionaryModal.props.allowDuplicates,
});
}} />
<label class='label is-unselectable' htmlFor='allowDuplicates'>
Allow Duplicate Words Allow Duplicate Words
{ ' ' }
<input type='checkbox'
defaultChecked={ allowDuplicates }
onChange={ (event) => {
editDictionaryModal.setState({
allowDuplicates: event.target.checked,
hasChanged: event.target.checked != editDictionaryModal.props.allowDuplicates,
});
}} />
</label> </label>
<div className='help'> <div className='help'>
Checking this box will allow any number of the exact same spelling of a word to be added Checking this box will allow any number of the exact same spelling of a word to be added
@ -50,20 +49,19 @@ export const EditSettingsForm = (props) => {
<div className='field'> <div className='field'>
<div className='control'> <div className='control'>
<label className='checkbox' <input className='is-checkradio is-rtl' type='checkbox' id='caseSensitive'
Disabled={ allowDuplicates ? 'disabled' : null } defaultChecked={ caseSensitive }
title={ allowDuplicates ? 'Disabled because allowing duplicates makes this unnecessary' : null}> disabled={ allowDuplicates }
onChange={ (event) => {
editDictionaryModal.setState({
caseSensitive: event.target.checked,
hasChanged: event.target.checked != editDictionaryModal.props.caseSensitive,
});
}} />
<label className='label is-unselectable' htmlFor='caseSensitive'
Disabled={allowDuplicates ? 'disabled' : null}
title={allowDuplicates ? 'Disabled because allowing duplicates makes this unnecessary' : null}>
Words are Case-Sensitive Words are Case-Sensitive
{ ' ' }
<input type='checkbox'
defaultChecked={ caseSensitive }
disabled={ allowDuplicates }
onChange={ (event) => {
editDictionaryModal.setState({
caseSensitive: event.target.checked,
hasChanged: event.target.checked != editDictionaryModal.props.caseSensitive,
});
}} />
</label> </label>
<div className='help'> <div className='help'>
Checking this box will allow any words spelled the same but with different capitalization to be added. Checking this box will allow any words spelled the same but with different capitalization to be added.
@ -73,17 +71,16 @@ export const EditSettingsForm = (props) => {
<div className='field'> <div className='field'>
<div className='control'> <div className='control'>
<label className='checkbox'> <input className='is-checkradio is-rtl' type='checkbox' id='sortByDefinition'
defaultChecked={ sortByDefinition }
onChange={ (event) => {
editDictionaryModal.setState({
sortByDefinition: event.target.checked,
hasChanged: event.target.checked != editDictionaryModal.props.sortByDefinition,
});
}} />
<label className='label is-unselectable' htmlFor='sortByDefinition'>
Sort by Definition Sort by Definition
{ ' ' }
<input type='checkbox'
defaultChecked={ sortByDefinition }
onChange={ (event) => {
editDictionaryModal.setState({
sortByDefinition: event.target.checked,
hasChanged: event.target.checked != editDictionaryModal.props.sortByDefinition,
});
}} />
</label> </label>
<div className='help'> <div className='help'>
Checking this box will sort the words in alphabetical order based on the Definition instead of the Word. Checking this box will sort the words in alphabetical order based on the Definition instead of the Word.
@ -93,20 +90,19 @@ export const EditSettingsForm = (props) => {
<div className='field'> <div className='field'>
<div className='control'> <div className='control'>
<label className='checkbox'> <input className='is-checkradio is-rtl' type='checkbox' id='isComplete'
defaultChecked={ isComplete }
onChange={ (event) => {
editDictionaryModal.setState({
isComplete: event.target.checked,
hasChanged: event.target.checked != editDictionaryModal.props.isComplete,
});
}} />
<label className='label is-unselectable' htmlFor='isComplete'>
Mark Complete Mark Complete
{ ' ' }
<input type='checkbox'
defaultChecked={ isComplete }
onChange={ (event) => {
editDictionaryModal.setState({
isComplete: event.target.checked,
hasChanged: event.target.checked != editDictionaryModal.props.isComplete,
});
}} />
</label> </label>
<div className='help'> <div className='help'>
Checking this box will mark your { specification } as "complete" and prevent any changes from being made. Checking this box will mark your { specification } as 'complete' and prevent any changes from being made.
</div> </div>
</div> </div>
</div> </div>
@ -115,17 +111,16 @@ export const EditSettingsForm = (props) => {
&& ( && (
<div className='field'> <div className='field'>
<div className='control'> <div className='control'>
<label className='checkbox'> <input className='is-checkradio is-rtl' type='checkbox' id='isPublic'
defaultChecked={ isPublic }
onChange={ (event) => {
editDictionaryModal.setState({
isPublic: event.target.checked,
hasChanged: event.target.checked != editDictionaryModal.props.isPublic,
});
}} />
<label className='label is-unselectable' htmlFor='isPublic'>
Make Public Make Public
{ ' ' }
<input type='checkbox'
defaultChecked={ isPublic }
onChange={ (event) => {
editDictionaryModal.setState({
isPublic: event.target.checked,
hasChanged: event.target.checked != editDictionaryModal.props.isPublic,
});
}} />
</label> </label>
<div className='help'> <div className='help'>
Checking this box will make your { specification } as public. Checking this box will make your { specification } as public.

View file

@ -13,6 +13,7 @@ const DISPLAY = {
DETAILS: 1, DETAILS: 1,
LINGUISTICS: 2, LINGUISTICS: 2,
SETTINGS: 3, SETTINGS: 3,
ACTIONS: 4,
} }
export class EditDictionaryModal extends Component { export class EditDictionaryModal extends Component {
@ -58,6 +59,8 @@ export class EditDictionaryModal extends Component {
hasChanged: false, hasChanged: false,
} }
this.modal = null;
} }
hasChanged () { hasChanged () {
@ -126,6 +129,14 @@ export class EditDictionaryModal extends Component {
); );
break; break;
} }
case DISPLAY.ACTIONS : {
displayJSX = (
<div class="content">
<p>Actions like import, export, delete, etc.</p>
</div>
);
}
} }
return ( return (
@ -135,7 +146,7 @@ export class EditDictionaryModal extends Component {
); );
} }
save () { save (callback = () => {}) {
const updatedDetails = {}; const updatedDetails = {};
if (this.state.name !== this.props.name) { if (this.state.name !== this.props.name) {
@ -243,7 +254,7 @@ export class EditDictionaryModal extends Component {
this.setState({ hasChanged: false }, () => { this.setState({ hasChanged: false }, () => {
// If setting that alters word display is changed, update the display. // If setting that alters word display is changed, update the display.
if (updatedDetails.hasOwnProperty('sortByDefinition')) { if (updatedDetails.hasOwnProperty('sortByDefinition')) {
this.props.updateDisplay(); this.props.updateDisplay(callback);
} }
}); });
}) })
@ -269,9 +280,16 @@ export class EditDictionaryModal extends Component {
> >
Save Save
</button> </button>
<button className='button'
disabled={ !hasChanged }
onClick={ () => this.save(this.modal.hide())}
>
Save & Close
</button>
</div> </div>
) )
} }
ref={(modal) => this.modal = modal}
> >
{!settings.isComplete {!settings.isComplete
@ -293,6 +311,11 @@ export class EditDictionaryModal extends Component {
Settings Settings
</a> </a>
</li> </li>
<li className={ (currentDisplay === DISPLAY.ACTIONS) ? 'is-active' : null }>
<a onClick={ this.toggleDisplay.bind(this, DISPLAY.ACTIONS) }>
Actions
</a>
</li>
</ul> </ul>
</div> </div>
), ),

View file

@ -23,7 +23,7 @@ export class SearchBox extends Component {
searchTerm: '', searchTerm: '',
caseSensitive: false, caseSensitive: false,
ignoreDiacritics: false, ignoreDiacritics: false,
filteredPartsOfSpeech: [...props.partsOfSpeech, 'Uncategorized'], filteredPartsOfSpeech: this.partsOfSpeechForFilter,
showHeader: false, showHeader: false,
showAdvanced: false, showAdvanced: false,
}; };
@ -62,7 +62,7 @@ export class SearchBox extends Component {
} }
togglePartOfSpeech (event) { togglePartOfSpeech (event) {
const uniquePartsOfSpeech = new Set(this.partsOfSpeechForFilter); const uniquePartsOfSpeech = new Set(this.state.filteredPartsOfSpeech);
if (event.target.checked) { if (event.target.checked) {
uniquePartsOfSpeech.add(event.target.value); uniquePartsOfSpeech.add(event.target.value);
} else { } else {
@ -111,7 +111,7 @@ export class SearchBox extends Component {
this.searchBox = input; this.searchBox = input;
}} }}
value={ this.state.searchTerm } value={ this.state.searchTerm }
onChange={(event) => { onInput={(event) => {
this.setState({ searchTerm: event.target.value.trim() }, () => this.search()); this.setState({ searchTerm: event.target.value.trim() }, () => this.search());
}} /> }} />
<span className='icon is-small is-left'> <span className='icon is-small is-left'>
@ -314,6 +314,8 @@ export class SearchBox extends Component {
showHeader () { showHeader () {
this.setState({ this.setState({
showHeader: true, showHeader: true,
}, () => {
this.searchBox.focus();
}); });
} }

View file

@ -109,7 +109,7 @@ export class WordForm extends Component {
<input className={ `input${(!this.state.nameIsValid) ? ' is-danger' : ''}` } <input className={ `input${(!this.state.nameIsValid) ? ' is-danger' : ''}` }
type='text' placeholder='Required' type='text' placeholder='Required'
value={ this.state.wordName } value={ this.state.wordName }
onChange={(event) => { onInput={(event) => {
this.setState({ wordName: event.target.value }); this.setState({ wordName: event.target.value });
}} /> }} />
{(!this.state.nameIsValid) {(!this.state.nameIsValid)
@ -149,7 +149,7 @@ export class WordForm extends Component {
<input className={ `input${(!this.state.definitionIsValid) ? ' is-danger' : ''}` } <input className={ `input${(!this.state.definitionIsValid) ? ' is-danger' : ''}` }
type='text' placeholder='Equivalent word(s)' type='text' placeholder='Equivalent word(s)'
value={ this.state.wordDefinition } value={ this.state.wordDefinition }
onChange={(event) => { onInput={(event) => {
this.setState({ wordDefinition: event.target.value }) this.setState({ wordDefinition: event.target.value })
}} /> }} />
{(!this.state.definitionIsValid) {(!this.state.definitionIsValid)

View file

@ -13,6 +13,8 @@ export class Modal extends Component {
children: PropTypes.oneOfType([PropTypes.object, PropTypes.array]), children: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
footerAlign: PropTypes.string, footerAlign: PropTypes.string,
footerContent: PropTypes.oneOfType([PropTypes.object, PropTypes.array]), footerContent: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
onShow: PropTypes.func,
onHide: PropTypes.func,
}, props, 'prop', 'Modal'); }, props, 'prop', 'Modal');
this.state = { this.state = {
@ -23,12 +25,20 @@ export class Modal extends Component {
show () { show () {
this.setState({ this.setState({
isVisible: true, isVisible: true,
}, () => {
if (this.props.onShow) {
this.props.onShow();
}
}); });
} }
hide () { hide () {
this.setState({ this.setState({
isVisible: false, isVisible: false,
}, () => {
if (this.props.onHide) {
this.props.onHide();
}
}); });
} }

View file

@ -1,24 +1,25 @@
import Inferno from 'inferno'; import Inferno from 'inferno';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import './styles.scss';
export const Pagination = (props) => { export const Pagination = (props) => {
PropTypes.checkPropTypes({ PropTypes.checkPropTypes({
currentPage: PropTypes.number.isRequired, currentPage: PropTypes.number.isRequired,
itemsPerPage: PropTypes.number.isRequired, itemsPerPage: PropTypes.number.isRequired,
stats: PropTypes.object.isRequired, stats: PropTypes.object.isRequired,
setPage: PropTypes.func.isRequired, setPage: PropTypes.func.isRequired,
wordsInCurrentList: PropTypes.number,
isTop: PropTypes.bool,
}, props, 'prop', 'Pagination'); }, props, 'prop', 'Pagination');
const { currentPage, itemsPerPage, stats, setPage } = props; const { currentPage, itemsPerPage, stats, setPage, wordsInCurrentList, isTop } = props;
const totalWords = stats.hasOwnProperty('numberOfWords') if (wordsInCurrentList === null) {
? stats.numberOfWords.find(group => group.name === 'Total').value : null; return null;
if (totalWords === null) {
return <div className="loader"></div>;
} }
const lastPage = Math.floor(totalWords / itemsPerPage); const lastPage = Math.floor(wordsInCurrentList / itemsPerPage);
const nextPage = currentPage + 1 > lastPage ? lastPage : currentPage + 1; const nextPage = currentPage + 1 > lastPage ? lastPage : currentPage + 1;
const prevPage = currentPage - 1 < 0 ? 0 : currentPage - 1; const prevPage = currentPage - 1 < 0 ? 0 : currentPage - 1;
@ -29,7 +30,7 @@ export const Pagination = (props) => {
} }
return ( return (
<nav className="pagination is-centered" role="navigation" aria-label="pagination"> <nav className={`pagination is-centered ${isTop ? 'is-top' : ''}`} role="navigation" aria-label="pagination">
<a className="pagination-previous" aria-label="Goto page 1" <a className="pagination-previous" aria-label="Goto page 1"
Disabled={currentPage === 0 ? 'disabled' : null} Disabled={currentPage === 0 ? 'disabled' : null}
onClick={() => changePage(prevPage)}> onClick={() => changePage(prevPage)}>

View file

@ -0,0 +1,3 @@
.pagination.is-top {
margin-bottom: 15px;
}

View file

@ -46,6 +46,8 @@ class App extends Component {
ignoreDiacritics: false, ignoreDiacritics: false,
filteredPartsOfSpeech: [...dictionary.partsOfSpeech, 'Uncategorized'], filteredPartsOfSpeech: [...dictionary.partsOfSpeech, 'Uncategorized'],
}, },
isLoadingWords: true,
wordsInCurrentList: null,
} }
this.updater = new Updater(this, dictionary); this.updater = new Updater(this, dictionary);
@ -98,7 +100,7 @@ class App extends Component {
const partsOfSpeechForFilter = [...partsOfSpeech, 'Uncategorized']; const partsOfSpeechForFilter = [...partsOfSpeech, 'Uncategorized'];
const pageStart = currentPage * itemsPerPage; const pageStart = currentPage * itemsPerPage;
const pageEnd = pageStart + itemsPerPage; const pageEnd = pageStart + itemsPerPage;
let displayedWords; let displayedWords = words;
if (this.isUsingFilter) { if (this.isUsingFilter) {
const { const {
searchingIn, searchingIn,
@ -109,10 +111,7 @@ class App extends Component {
filteredPartsOfSpeech filteredPartsOfSpeech
} = searchConfig; } = searchConfig;
displayedWords = words.filter((word, index) => { displayedWords = displayedWords.filter((word, index) => {
if (index < pageStart || index >= pageEnd) {
return false;
}
const wordPartOfSpeech = word.partOfSpeech === '' ? 'Uncategorized' : word.partOfSpeech; const wordPartOfSpeech = word.partOfSpeech === '' ? 'Uncategorized' : word.partOfSpeech;
if (!filteredPartsOfSpeech.includes(wordPartOfSpeech)) { if (!filteredPartsOfSpeech.includes(wordPartOfSpeech)) {
return false; return false;
@ -168,30 +167,36 @@ class App extends Component {
} }
} }
}); });
} else {
displayedWords = words.filter((word, index) => {
if (index < pageStart || index >= pageEnd) {
return false;
}
return true;
});
} }
const wordsInCurrentList = displayedWords.length;
displayedWords = displayedWords.filter((word, index) => {
if (index < pageStart || index >= pageEnd) {
return false;
}
return true;
});
this.setState({ this.setState({
displayedWords, displayedWords,
stats: getWordsStats(words, partsOfSpeech, this.state.settings.caseSensitive), stats: getWordsStats(words, partsOfSpeech, this.state.settings.caseSensitive),
wordsInCurrentList,
isLoadingWords: false,
}, () => callback()); }, () => callback());
}); });
} }
search (searchConfig) { search (searchConfig) {
this.setState({ this.setState({
isLoadingWords: true,
searchConfig: searchConfig, searchConfig: searchConfig,
}, () => this.updateDisplayedWords()); }, () => this.updateDisplayedWords());
} }
setPage (newPage) { setPage (newPage) {
this.setState({ this.setState({
isLoadingWords: true,
currentPage: newPage, currentPage: newPage,
}, () => this.updateDisplayedWords()); }, () => this.updateDisplayedWords());
} }
@ -207,8 +212,10 @@ class App extends Component {
<MainDisplay <MainDisplay
dictionaryInfo={ this.dictionaryInfo } dictionaryInfo={ this.dictionaryInfo }
isLoadingWords={ this.state.isLoadingWords }
wordsToDisplay={ this.state.displayedWords } wordsToDisplay={ this.state.displayedWords }
wordsAreFiltered={ this.isUsingFilter } wordsAreFiltered={ this.isUsingFilter }
wordsInCurrentList={ this.state.wordsInCurrentList }
currentPage={ this.state.currentPage } currentPage={ this.state.currentPage }
itemsPerPage={ this.state.itemsPerPage } itemsPerPage={ this.state.itemsPerPage }
stats={ this.state.stats } stats={ this.state.stats }

View file

@ -5,6 +5,7 @@ $fa-font-path: "~font-awesome/fonts/";
@import 'bulma-overrides'; @import 'bulma-overrides';
@import '../../node_modules/bulma/bulma'; @import '../../node_modules/bulma/bulma';
@import '../../node_modules/bulma-checkradio/dist/bulma-checkradio';
@import '../../node_modules/sweetalert2/dist/sweetalert2'; @import '../../node_modules/sweetalert2/dist/sweetalert2';

View file

@ -1002,6 +1002,10 @@ builtin-status-codes@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8"
bulma-checkradio@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/bulma-checkradio/-/bulma-checkradio-1.0.1.tgz#21674a9aee65c48e4028594bd0f22e5b52ea5346"
bulma@^0.6.2: bulma@^0.6.2:
version "0.6.2" version "0.6.2"
resolved "https://registry.yarnpkg.com/bulma/-/bulma-0.6.2.tgz#f4b1d11d5acc51a79644eb0a2b0b10649d3d71f5" resolved "https://registry.yarnpkg.com/bulma/-/bulma-0.6.2.tgz#f4b1d11d5acc51a79644eb0a2b0b10649d3d71f5"