diff options
Diffstat (limited to 'src/client/react')
15 files changed, 194 insertions, 144 deletions
diff --git a/src/client/react/LandingPage.js b/src/client/react/LandingPage.tsx index d79826e..f8bb58c 100644 --- a/src/client/react/LandingPage.js +++ b/src/client/react/LandingPage.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import Search from './components/container/Search'; const App = () => ( diff --git a/src/client/react/actions/search.js b/src/client/react/actions/search.js deleted file mode 100644 index 1b6847d..0000000 --- a/src/client/react/actions/search.js +++ /dev/null @@ -1,14 +0,0 @@ -export const inputChange = typedValue => ({ - type: 'SEARCH/INPUT_CHANGE', - typedValue, -}); - -/** - * Change the selected result. - * @param {+1/-1} relativeChange usually +1 or -1, the change relative to the - * current result. - */ -export const changeSelectedResult = relativeChange => ({ - type: 'SEARCH/CHANGE_SELECTED_RESULT', - relativeChange, -}); diff --git a/src/client/react/actions/search.ts b/src/client/react/actions/search.ts new file mode 100644 index 0000000..45c31fb --- /dev/null +++ b/src/client/react/actions/search.ts @@ -0,0 +1,24 @@ +export interface InputChangeAction { + type: 'SEARCH/INPUT_CHANGE', + typedValue: string, +} + +export const inputChange = (typedValue: string): InputChangeAction => ({ + type: 'SEARCH/INPUT_CHANGE', + typedValue, +}); + +export interface ChangeSelectedResultAction { + type: 'SEARCH/CHANGE_SELECTED_RESULT', + relativeChange: 1 | -1, +} + +/** + * Change the selected result. + * @param {+1/-1} relativeChange usually +1 or -1, the change relative to the + * current result. + */ +export const changeSelectedResult = (relativeChange: 1 | -1): ChangeSelectedResultAction => ({ + type: 'SEARCH/CHANGE_SELECTED_RESULT', + relativeChange, +}); diff --git a/src/client/react/components/container/Results.js b/src/client/react/components/container/Results.js deleted file mode 100644 index 911ea27..0000000 --- a/src/client/react/components/container/Results.js +++ /dev/null @@ -1,35 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; -import classnames from 'classnames'; -import Result from '../presentational/Result'; - -const Results = (({ results, isExactMatch, selectedResult }) => ( - <div - className={classnames('search__results', { - 'search__results--has-results': !isExactMatch && results.length > 0, - })} - > - {!isExactMatch && results.map(userId => ( - <Result key={userId} userId={userId} isSelected={userId === selectedResult} /> - ))} - </div> -)); - -Results.propTypes = { - results: PropTypes.arrayOf(PropTypes.string).isRequired, - isExactMatch: PropTypes.bool.isRequired, - selectedResult: PropTypes.string, -}; - -Results.defaultProps = { - selectedResult: null, -}; - -const mapStateToProps = state => ({ - results: state.search.results, - isExactMatch: state.search.isExactMatch, - selectedResult: state.search.selectedResult, -}); - -export default connect(mapStateToProps)(Results); diff --git a/src/client/react/components/container/Results.tsx b/src/client/react/components/container/Results.tsx new file mode 100644 index 0000000..21d3378 --- /dev/null +++ b/src/client/react/components/container/Results.tsx @@ -0,0 +1,26 @@ +import * as React from 'react'; +import { connect } from 'react-redux'; +import * as classnames from 'classnames'; +import Result from '../presentational/Result'; +import { User } from '../../users'; +import { State } from '../../reducers'; + +const Results: React.StatelessComponent<{ results: string[], isExactMatch: boolean, selectedResult: string }> = (props) => ( + <div + className={classnames('search__results', { + 'search__results--has-results': !props.isExactMatch && props.results.length > 0, + })} + > + {!props.isExactMatch && props.results.map(userId => ( + <Result key={userId} userId={userId} isSelected={userId === props.selectedResult} /> + ))} + </div> +); + +const mapStateToProps = (state: State) => ({ + results: state.search.results, + isExactMatch: state.search.isExactMatch, + selectedResult: state.search.selectedResult, +}); + +export default connect(mapStateToProps)(Results); diff --git a/src/client/react/components/container/Search.js b/src/client/react/components/container/Search.tsx index e49e6a7..fdd6c83 100644 --- a/src/client/react/components/container/Search.js +++ b/src/client/react/components/container/Search.tsx @@ -1,18 +1,30 @@ -import React from 'react'; -import PropTypes from 'prop-types'; +import * as React from 'react'; +import { Dispatch } from 'redux'; import { connect } from 'react-redux'; -import classnames from 'classnames'; +import * as classnames from 'classnames'; -import SearchIcon from 'react-icons/lib/md/search'; +import SearchIcon = require('react-icons/lib/md/search'); import { inputChange, changeSelectedResult } from '../../actions/search'; +import { Action } from '../../reducers/search'; +import { State } from '../../reducers'; import users from '../../users'; import Results from './Results'; import IconFromUserType from '../presentational/IconFromUserType'; -class Search extends React.Component { - constructor(props) { +interface SearchStatehProps { + selectedResult: string, + isExactMatch: boolean, +} + +interface SearchDispatchProps { + changeSelectedResult(relativeChange: 1 | -1): void, + inputChange(typedValue: string): void, +} + +class Search extends React.Component<SearchStatehProps & SearchDispatchProps, any> { + constructor(props: SearchStatehProps & SearchDispatchProps) { super(props); this.state = { @@ -36,15 +48,15 @@ class Search extends React.Component { }); } - onKeyDown(event) { + onKeyDown(event: React.KeyboardEvent<any>) { if (event.key === 'ArrowUp' || event.key === 'ArrowDown') { event.preventDefault(); switch (event.key) { case 'ArrowUp': - this.props.dispatch(changeSelectedResult(-1)); + this.props.changeSelectedResult(-1); break; case 'ArrowDown': - this.props.dispatch(changeSelectedResult(+1)); + this.props.changeSelectedResult(+1); break; default: throw new Error('This should never happen... pls?'); @@ -56,7 +68,7 @@ class Search extends React.Component { const { selectedResult, isExactMatch, - dispatch, + inputChange, } = this.props; const { @@ -74,7 +86,7 @@ class Search extends React.Component { </div> <input id="search__input" - onChange={event => dispatch(inputChange(event.target.value))} + onChange={event => inputChange(event.target.value)} onKeyDown={this.onKeyDown} placeholder="Zoeken" onFocus={this.onFocus} @@ -87,20 +99,33 @@ class Search extends React.Component { } } -Search.propTypes = { - selectedResult: PropTypes.string, - isExactMatch: PropTypes.bool.isRequired, - dispatch: PropTypes.func.isRequired, -}; +// Search.propTypes = { +// selectedResult: PropTypes.string, +// isExactMatch: PropTypes.bool.isRequired, +// dispatch: PropTypes.func.isRequired, +// }; -Search.defaultProps = { - selectedResult: null, -}; +// Search.defaultProps = { +// selectedResult: null, +// }; -const mapStateToProps = state => ({ - results: state.search.results, +const mapStateToProps = (state: State):SearchStatehProps => ({ selectedResult: state.search.selectedResult, isExactMatch: state.search.isExactMatch, }); -export default connect(mapStateToProps)(Search); +// const mapDispatchToProps = { +// inputChange, +// changeSelectedResult, +// }; + +const mapDispatchToProps = (dispatch: any): SearchDispatchProps => ({ + inputChange(typedValue) { + dispatch(inputChange(typedValue)); + }, + changeSelectedResult(relativeChange) { + dispatch(changeSelectedResult(relativeChange)) + } +}); + +export default connect(mapStateToProps, mapDispatchToProps)(Search); diff --git a/src/client/react/components/presentational/IconFromUserType.js b/src/client/react/components/presentational/IconFromUserType.js deleted file mode 100644 index ee0e04b..0000000 --- a/src/client/react/components/presentational/IconFromUserType.js +++ /dev/null @@ -1,37 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import StudentIcon from 'react-icons/lib/md/person'; -import RoomIcon from 'react-icons/lib/md/room'; -import ClassIcon from 'react-icons/lib/md/group'; -import TeacherIcon from 'react-icons/lib/md/account-circle'; - -const IconFromUserType = ({ userType, defaultIcon }) => { - switch (userType) { - case 'c': - return <ClassIcon />; - case 't': - return <TeacherIcon />; - case 's': - return <StudentIcon />; - case 'r': - return <RoomIcon />; - default: - if (defaultIcon) { - return defaultIcon; - } - - throw new Error('`userType` was invalid or not given, but `defaultIcon` is not defined.'); - } -}; - -IconFromUserType.propTypes = { - userType: PropTypes.string, - defaultIcon: PropTypes.element, -}; - -IconFromUserType.defaultProps = { - userType: null, - defaultIcon: null, -}; - -export default IconFromUserType; diff --git a/src/client/react/components/presentational/IconFromUserType.tsx b/src/client/react/components/presentational/IconFromUserType.tsx new file mode 100644 index 0000000..d77ea1b --- /dev/null +++ b/src/client/react/components/presentational/IconFromUserType.tsx @@ -0,0 +1,31 @@ +import * as React from 'react'; +import StudentIcon = require('react-icons/lib/md/person'); +import RoomIcon = require('react-icons/lib/md/room'); +import ClassIcon = require('react-icons/lib/md/group'); +import TeacherIcon = require('react-icons/lib/md/account-circle'); + +// interface IconFromUserTypeProps { +// userType: string, +// defaultIcon?: JSX.Element, +// } + +const IconFromUserType: React.StatelessComponent<{ userType: string, defaultIcon?: JSX.Element }> = (props) => { + switch (props.userType) { + case 'c': + return <ClassIcon />; + case 't': + return <TeacherIcon />; + case 's': + return <StudentIcon />; + case 'r': + return <RoomIcon />; + default: + if (props.defaultIcon) { + return props.defaultIcon; + } + + throw new Error('`userType` was invalid or not given, but `defaultIcon` is not defined.'); + } +}; + +export default IconFromUserType; diff --git a/src/client/react/components/presentational/Result.js b/src/client/react/components/presentational/Result.tsx index 0b9e024..b33a365 100644 --- a/src/client/react/components/presentational/Result.js +++ b/src/client/react/components/presentational/Result.tsx @@ -1,11 +1,10 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import classnames from 'classnames'; +import * as React from 'react'; +import * as classnames from 'classnames'; import users from '../../users'; import IconFromUserType from './IconFromUserType'; -const Result = ({ userId, isSelected }) => ( +const Result: React.StatelessComponent<{ userId: string, isSelected: boolean }> = ({ userId, isSelected }) => ( <div className={classnames('search__result', { 'search__result--selected': isSelected, @@ -16,9 +15,4 @@ const Result = ({ userId, isSelected }) => ( </div> ); -Result.propTypes = { - userId: PropTypes.string.isRequired, - isSelected: PropTypes.bool.isRequired, -}; - export default Result; diff --git a/src/client/react/index.js b/src/client/react/index.tsx index 5279bf4..f0c3226 100644 --- a/src/client/react/index.js +++ b/src/client/react/index.tsx @@ -1,5 +1,5 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; import { Provider } from 'react-redux'; import { BrowserRouter as Router, Route } from 'react-router-dom'; import { createStore, applyMiddleware, compose } from 'redux'; @@ -9,10 +9,10 @@ import reducer from './reducers'; import LandingPage from './LandingPage'; // eslint-disable-next-line no-underscore-dangle -const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; +// const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; const store = createStore( reducer, - composeEnhancers(applyMiddleware(logger, thunk)), + compose(applyMiddleware(logger, thunk)), ); ReactDOM.render( diff --git a/src/client/react/reducers.js b/src/client/react/reducers.js deleted file mode 100644 index 9fdf2c4..0000000 --- a/src/client/react/reducers.js +++ /dev/null @@ -1,8 +0,0 @@ -import { combineReducers } from 'redux'; -import search from './reducers/search'; - -const rootReducer = combineReducers({ - search, -}); - -export default rootReducer; diff --git a/src/client/react/reducers.ts b/src/client/react/reducers.ts new file mode 100644 index 0000000..254fe76 --- /dev/null +++ b/src/client/react/reducers.ts @@ -0,0 +1,12 @@ +import { combineReducers } from 'redux'; +import search, { State as SearchState } from './reducers/search'; + +export interface State { + search: SearchState, +} + +const rootReducer = combineReducers<State>({ + search, +}); + +export default rootReducer; diff --git a/src/client/react/reducers/search.test.js b/src/client/react/reducers/search.test.ts index e0ca18e..5869b81 100644 --- a/src/client/react/reducers/search.test.js +++ b/src/client/react/reducers/search.test.ts @@ -1,4 +1,4 @@ -window.USERS = [ +(<any>window).USERS = [ { type: 's', value: '18561' }, { type: 's', value: '18562' }, { type: 's', value: '18563' }, diff --git a/src/client/react/reducers/search.js b/src/client/react/reducers/search.ts index 2a7e7a5..658d3ca 100644 --- a/src/client/react/reducers/search.js +++ b/src/client/react/reducers/search.ts @@ -1,7 +1,16 @@ -import fuzzy from 'fuzzy'; -import users from '../users'; +import * as fuzzy from 'fuzzy'; +import users, { User } from '../users'; +import { InputChangeAction, ChangeSelectedResultAction } from '../actions/search'; + +export interface State { + results: string[], + selectedResult: string | null, + isExactMatch: boolean, +}; + +export type Action = InputChangeAction | ChangeSelectedResultAction; -const DEFAULT_STATE = { +const DEFAULT_STATE: State = { results: [ 's/18562', ], @@ -9,22 +18,22 @@ const DEFAULT_STATE = { isExactMatch: false, }; -function getSearchResults(allUsers, query) { +function getSearchResults(allUsers: User[], query: string) { if (query.trim() === '') { return []; } const allResults = fuzzy.filter(query, allUsers, { - extract: user => user.value, + extract: (user: User) => user.value, }); const firstResults = allResults.splice(0, 4); - const userIds = firstResults.map(result => result.original.id); + const userIds = firstResults.map((result: { original: User }) => result.original.id); return userIds; } -const search = (state = DEFAULT_STATE, action) => { +const search = (state = DEFAULT_STATE, action: Action): State => { switch (action.type) { case 'SEARCH/INPUT_CHANGE': { const results = getSearchResults(users.allUsers, action.typedValue); diff --git a/src/client/react/users.js b/src/client/react/users.ts index 01ff093..a80a1c5 100644 --- a/src/client/react/users.js +++ b/src/client/react/users.ts @@ -2,9 +2,26 @@ import { combineReducers, createStore } from 'redux'; -const getId = ({ type, value }) => `${type}/${value}`; +export interface User { + type: string, + value: string, + id: string, +} -const byId = (state = {}, action) => { +type Action = { + type: 'USERS/ADD_USER', + user: User, +} + +declare global { + interface Window { + USERS: User[]; + } +} + +const getId = ({ type, value }: User) => `${type}/${value}`; + +const byId = (state = {}, action: Action) => { switch (action.type) { case 'USERS/ADD_USER': return { @@ -18,7 +35,7 @@ const byId = (state = {}, action) => { } }; -const allIds = (state = [], action) => { +const allIds = (state : any[] = [], action : Action) => { switch (action.type) { case 'USERS/ADD_USER': return [ @@ -30,7 +47,7 @@ const allIds = (state = [], action) => { } }; -const allUsers = (state = [], action) => { +const allUsers = (state : any[] = [], action : Action) => { switch (action.type) { case 'USERS/ADD_USER': return [ @@ -44,13 +61,19 @@ const allUsers = (state = [], action) => { } }; -const store = createStore(combineReducers({ +interface State { + byId: any, + allIds: string[], + allUsers: User[] +} + +const store = createStore(combineReducers<State>({ byId, allIds, allUsers, })); -USERS.forEach((user) => { +window.USERS.forEach((user) => { store.dispatch({ type: 'USERS/ADD_USER', user: { |