diff --git a/package.json b/package.json index 484da52..1681f7b 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "dependencies": { "babel-polyfill": "^6.26.0", "bulma": "^0.6.2", + "bulma-checkradio": "^1.0.1", "dexie": "^2.0.1", "font-awesome": "^4.7.0", "inferno": "^4.0.4", diff --git a/public/api/User.php b/public/api/User.php index fbcded5..0e935e0 100644 --- a/public/api/User.php +++ b/public/api/User.php @@ -24,7 +24,10 @@ class User { } } else if (password_verify($password, $user['password'])) { $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; @@ -60,13 +63,57 @@ VALUES (?, ?, ?, ?, ?, '. time() .')'; $new_dictionary = $this->dictionary->create($new_user_id); 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; } + 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) { $user_data = $this->token->decode($token); if ($user_data !== false) { diff --git a/public/api/index.php b/public/api/index.php index 79b6e04..e684dca 100644 --- a/public/api/index.php +++ b/public/api/index.php @@ -12,10 +12,10 @@ switch ($action) { case 'login': { if (isset($request['email']) && isset($request['password'])) { $user = new User(); - $token = $user->logIn($request['email'], $request['password']); - if ($token !== false) { + $user_data = $user->logIn($request['email'], $request['password']); + if ($user_data !== false) { return Response::json(array( - 'data' => $token, + 'data' => $user_data, 'error' => false, ), 200); } @@ -33,10 +33,10 @@ switch ($action) { if (isset($request['email']) && isset($request['password'])) { $user = new User(); if (!$user->emailExists($request['email'])) { - $token = $user->create($request['email'], $request['password'], $request['userData']); - if ($token !== false) { + $user_data = $user->create($request['email'], $request['password'], $request['userData']); + if ($user_data !== false) { return Response::json(array( - 'data' => $token, + 'data' => $user_data, 'error' => false, ), 201); } @@ -103,6 +103,26 @@ switch ($action) { 'error' => true, ), 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': { if ($token !== false) { $user = new User(); diff --git a/src/components/MainDisplay.jsx b/src/components/MainDisplay.jsx index 51cae62..28bca00 100644 --- a/src/components/MainDisplay.jsx +++ b/src/components/MainDisplay.jsx @@ -16,8 +16,10 @@ export class MainDisplay extends Component { PropTypes.checkPropTypes({ dictionaryInfo: PropTypes.object.isRequired, + isLoadingWords: PropTypes.bool, wordsToDisplay: PropTypes.array.isRequired, wordsAreFiltered: PropTypes.bool, + wordsInCurrentList: PropTypes.number, currentPage: PropTypes.number, itemsPerPage: PropTypes.number, stats: PropTypes.object.isRequired, @@ -55,8 +57,10 @@ export class MainDisplay extends Component { render () { const { dictionaryInfo, + isLoadingWords, wordsToDisplay, wordsAreFiltered, + wordsInCurrentList, currentPage, itemsPerPage, stats, @@ -105,7 +109,16 @@ export class MainDisplay extends Component { )} + + @@ -114,7 +127,8 @@ export class MainDisplay extends Component { currentPage={currentPage} itemsPerPage={itemsPerPage} stats={stats} - setPage={ setPage } /> + setPage={setPage} + wordsInCurrentList={wordsInCurrentList} /> diff --git a/src/components/display/WordsList.jsx b/src/components/display/WordsList.jsx index 98bfa6e..058b4ad 100644 --- a/src/components/display/WordsList.jsx +++ b/src/components/display/WordsList.jsx @@ -13,6 +13,7 @@ export class WordsList extends Component { super(props); PropTypes.checkPropTypes({ + isLoadingWords: PropTypes.bool, adsEveryXWords: PropTypes.number, words: PropTypes.array, updateDisplay: PropTypes.func.isRequired, @@ -22,6 +23,12 @@ export class WordsList extends Component { render () { const adsEveryXWords = this.props.adsEveryXWords || 10; + if (this.props.isLoadingWords) { + return
+
+
; + } + return (
diff --git a/src/components/management/AccountManager/MyAccount.jsx b/src/components/management/AccountManager/MyAccount.jsx new file mode 100644 index 0000000..085abe6 --- /dev/null +++ b/src/components/management/AccountManager/MyAccount.jsx @@ -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 ( +
+
+ +
+

Account Details

+
+ Email: {this.state.email} +
+
+ Username: {this.state.username} +
+
+ +
+ {this.setState({publicName: event.target.value})}} /> +
+
+
+
+ { this.setState({ allowEmails: !this.state.allowEmails }) }} /> + +
+
+
+ { this.setState({ useIPAPronunciation: !this.state.useIPAPronunciation }) }} /> + +
+
+ +
+

Account Actions

+
+ +
+
+ +
+
+
+
+ +
+
+ ); + } +} \ No newline at end of file diff --git a/src/components/management/AccountManager/index.jsx b/src/components/management/AccountManager/index.jsx index d76d864..c01f855 100644 --- a/src/components/management/AccountManager/index.jsx +++ b/src/components/management/AccountManager/index.jsx @@ -6,9 +6,18 @@ import store from 'store'; import { Modal } from '../../structure/Modal'; import { LoginForm } from './LoginForm'; +import { MyAccount } from './MyAccount'; import { request } from '../../../Helpers'; +const defaultUserData = { + email: '', + username: '', + publicName: '', + allowEmails: true, + useIPAPronunciation: true, +}; + export class AccountManager extends Component { constructor (props) { super(props); @@ -17,9 +26,21 @@ export class AccountManager extends Component { updater: PropTypes.object.isRequired, }, props, 'prop', 'AccountManager'); + const userData = store.get('LexicongaUserData'); + this.state = { 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) { @@ -28,7 +49,12 @@ export class AccountManager extends Component { logOut () { store.remove('LexicongaToken'); - this.setState({ isLoggedIn: false }); + store.remove('LexicongaUserData'); + this.setState({ + isLoggedIn: false, + userData: Object.assign({}, defaultUserData), + userDictionaries: [], + }); } signUp (email, password, userData) { @@ -44,23 +70,69 @@ export class AccountManager extends Component { if (error) { console.error(data); } else { - store.set('LexicongaToken', data); - this.setState({ isLoggedIn: true }, () => { + store.set('LexicongaToken', data.token); + store.set('LexicongaUserData', data.user); + this.setState({ + isLoggedIn: true, + userData: data.user, + }, () => { + this.getDictionaryNames(); 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 () { const token = store.get('LexicongaToken'); if (token) { + const { userData } = this.state; + return (
- -
-

Hello My Account!

-
+ + {} } /> Log Out diff --git a/src/components/management/EditDictionaryModal/EditSettingsForm.jsx b/src/components/management/EditDictionaryModal/EditSettingsForm.jsx index f79f53c..12b3ddd 100644 --- a/src/components/management/EditDictionaryModal/EditSettingsForm.jsx +++ b/src/components/management/EditDictionaryModal/EditSettingsForm.jsx @@ -30,17 +30,16 @@ export const EditSettingsForm = (props) => {
-
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) => {
-
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) => {
-
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) => {
-
@@ -115,17 +111,16 @@ export const EditSettingsForm = (props) => { && (
-
  • + + Actions + +
  • ), diff --git a/src/components/management/SearchBox/index.jsx b/src/components/management/SearchBox/index.jsx index cd62708..716a0d9 100644 --- a/src/components/management/SearchBox/index.jsx +++ b/src/components/management/SearchBox/index.jsx @@ -23,7 +23,7 @@ export class SearchBox extends Component { searchTerm: '', caseSensitive: false, ignoreDiacritics: false, - filteredPartsOfSpeech: [...props.partsOfSpeech, 'Uncategorized'], + filteredPartsOfSpeech: this.partsOfSpeechForFilter, showHeader: false, showAdvanced: false, }; @@ -62,7 +62,7 @@ export class SearchBox extends Component { } togglePartOfSpeech (event) { - const uniquePartsOfSpeech = new Set(this.partsOfSpeechForFilter); + const uniquePartsOfSpeech = new Set(this.state.filteredPartsOfSpeech); if (event.target.checked) { uniquePartsOfSpeech.add(event.target.value); } else { @@ -111,7 +111,7 @@ export class SearchBox extends Component { this.searchBox = input; }} value={ this.state.searchTerm } - onChange={(event) => { + onInput={(event) => { this.setState({ searchTerm: event.target.value.trim() }, () => this.search()); }} /> @@ -314,6 +314,8 @@ export class SearchBox extends Component { showHeader () { this.setState({ showHeader: true, + }, () => { + this.searchBox.focus(); }); } diff --git a/src/components/management/WordForm.jsx b/src/components/management/WordForm.jsx index d51fb5f..6feffea 100644 --- a/src/components/management/WordForm.jsx +++ b/src/components/management/WordForm.jsx @@ -109,7 +109,7 @@ export class WordForm extends Component { { + onInput={(event) => { this.setState({ wordName: event.target.value }); }} /> {(!this.state.nameIsValid) @@ -149,7 +149,7 @@ export class WordForm extends Component { { + onInput={(event) => { this.setState({ wordDefinition: event.target.value }) }} /> {(!this.state.definitionIsValid) diff --git a/src/components/structure/Modal.jsx b/src/components/structure/Modal.jsx index e8f99fb..2acf886 100644 --- a/src/components/structure/Modal.jsx +++ b/src/components/structure/Modal.jsx @@ -13,6 +13,8 @@ export class Modal extends Component { children: PropTypes.oneOfType([PropTypes.object, PropTypes.array]), footerAlign: PropTypes.string, footerContent: PropTypes.oneOfType([PropTypes.object, PropTypes.array]), + onShow: PropTypes.func, + onHide: PropTypes.func, }, props, 'prop', 'Modal'); this.state = { @@ -23,12 +25,20 @@ export class Modal extends Component { show () { this.setState({ isVisible: true, + }, () => { + if (this.props.onShow) { + this.props.onShow(); + } }); } hide () { this.setState({ isVisible: false, + }, () => { + if (this.props.onHide) { + this.props.onHide(); + } }); } diff --git a/src/components/structure/Pagination.jsx b/src/components/structure/Pagination/index.jsx similarity index 77% rename from src/components/structure/Pagination.jsx rename to src/components/structure/Pagination/index.jsx index e57fa12..f0ac101 100644 --- a/src/components/structure/Pagination.jsx +++ b/src/components/structure/Pagination/index.jsx @@ -1,24 +1,25 @@ import Inferno from 'inferno'; import PropTypes from 'prop-types'; +import './styles.scss'; + export const Pagination = (props) => { PropTypes.checkPropTypes({ currentPage: PropTypes.number.isRequired, itemsPerPage: PropTypes.number.isRequired, stats: PropTypes.object.isRequired, setPage: PropTypes.func.isRequired, + wordsInCurrentList: PropTypes.number, + isTop: PropTypes.bool, }, props, 'prop', 'Pagination'); - const { currentPage, itemsPerPage, stats, setPage } = props; - - const totalWords = stats.hasOwnProperty('numberOfWords') - ? stats.numberOfWords.find(group => group.name === 'Total').value : null; + const { currentPage, itemsPerPage, stats, setPage, wordsInCurrentList, isTop } = props; - if (totalWords === null) { - return
    ; + if (wordsInCurrentList === null) { + return null; } - const lastPage = Math.floor(totalWords / itemsPerPage); + const lastPage = Math.floor(wordsInCurrentList / itemsPerPage); const nextPage = currentPage + 1 > lastPage ? lastPage : currentPage + 1; const prevPage = currentPage - 1 < 0 ? 0 : currentPage - 1; @@ -29,7 +30,7 @@ export const Pagination = (props) => { } return ( -