aboutsummaryrefslogtreecommitdiff
path: root/src/client/react/reducers
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/react/reducers')
-rw-r--r--src/client/react/reducers/search.js105
-rw-r--r--src/client/react/reducers/search.test.js229
-rw-r--r--src/client/react/reducers/view.js53
3 files changed, 387 insertions, 0 deletions
diff --git a/src/client/react/reducers/search.js b/src/client/react/reducers/search.js
new file mode 100644
index 0000000..770cdcb
--- /dev/null
+++ b/src/client/react/reducers/search.js
@@ -0,0 +1,105 @@
+import fuzzy from 'fuzzy';
+import users from '../users';
+
+const DEFAULT_STATE = {
+ // results: [
+ // 's/18562',
+ // ],
+ results: [],
+ searchText: '',
+ selectedResult: null,
+ isExactMatch: false,
+};
+
+function getSearchResults(allUsers, query) {
+ if (query.trim() === '') {
+ return [];
+ }
+
+ const allResults = fuzzy.filter(query, allUsers, {
+ extract: user => user.value,
+ });
+
+ const firstResults = allResults.splice(0, 4);
+ const userIds = firstResults.map(result => result.original.id);
+
+ return userIds;
+}
+
+const search = (state = DEFAULT_STATE, action) => {
+ switch (action.type) {
+ case 'SEARCH/SET_USER': {
+ const { user } = action;
+
+ if (user == null) {
+ return DEFAULT_STATE;
+ }
+
+ return {
+ ...state,
+ results: [],
+ searchText: users.byId[user].value,
+ selectedResult: user,
+ isExactMatch: true,
+ };
+ }
+
+ case 'SEARCH/INPUT_CHANGE': {
+ const { searchText } = action;
+ const results = getSearchResults(users.allUsers, action.searchText);
+ let selectedResult = null;
+ let isExactMatch = false;
+
+ // Is the typed value exactly the same as the first result? Then show the
+ // appropriate icon instead of the generic search icon.
+ if ((results.length === 1) && (action.searchText === users.byId[results[0]].value)) {
+ [selectedResult] = results;
+ isExactMatch = true;
+ }
+
+ return {
+ ...state,
+ results,
+ searchText,
+ selectedResult,
+ isExactMatch,
+ };
+ }
+
+ case 'SEARCH/CHANGE_SELECTED_RESULT': {
+ const { results, isExactMatch } = state;
+
+ if (isExactMatch) return state;
+
+ const prevSelectedResult = state.selectedResult;
+ const prevSelectedResultIndex = results.indexOf(prevSelectedResult);
+ let nextSelectedResultIndex =
+ prevSelectedResultIndex + action.relativeChange;
+
+ if (nextSelectedResultIndex < -1) {
+ nextSelectedResultIndex = results.length - 1;
+ } else if (nextSelectedResultIndex > results.length - 1) {
+ nextSelectedResultIndex = -1;
+ }
+
+ const nextSelectedResult =
+ nextSelectedResultIndex === -1
+ ? null
+ : results[nextSelectedResultIndex];
+
+ return {
+ ...state,
+ selectedResult: nextSelectedResult,
+ };
+ }
+
+ default:
+ return state;
+ }
+};
+
+export default search;
+
+export const _test = {
+ DEFAULT_STATE,
+};
diff --git a/src/client/react/reducers/search.test.js b/src/client/react/reducers/search.test.js
new file mode 100644
index 0000000..22d32e2
--- /dev/null
+++ b/src/client/react/reducers/search.test.js
@@ -0,0 +1,229 @@
+window.USERS = [
+ { type: 's', value: '18561' },
+ { type: 's', value: '18562' },
+ { type: 's', value: '18563' },
+ { type: 's', value: '18564' },
+ { type: 's', value: '18565' },
+ { type: 's', value: '18566' },
+ { type: 's', value: '18567' },
+ { type: 's', value: '18568' },
+ { type: 's', value: '18569' },
+];
+
+const deepFreeze = require('deep-freeze');
+const search = require('./search').default;
+const { _test } = require('./search');
+const {
+ setUser,
+ inputChange,
+ changeSelectedResult,
+} = require('../actions/search');
+
+describe('reducers', () => {
+ describe('search', () => {
+ describe('SEARCH/SET_USER', () => {
+ it('Resets to the default state if the user is null', () => {
+ expect(search({ foo: 'bar' }, setUser(null))).toEqual(_test.DEFAULT_STATE);
+ });
+
+ it('Sets all the values of that user properly', () => {
+ expect(search(undefined, setUser('s/18561'))).toEqual({
+ results: [],
+ searchText: '18561',
+ selectedResult: 's/18561',
+ isExactMatch: true,
+ });
+ });
+ });
+
+ describe('SEARCH/INPUT_CHANGE', () => {
+ it('Returns no results when nothing is typed in', () => {
+ expect(search(undefined, inputChange(''))).toEqual({
+ results: [],
+ searchText: '',
+ selectedResult: null,
+ isExactMatch: false,
+ });
+ });
+
+ it('Returns no results when a space is typed in', () => {
+ expect(search(undefined, inputChange(' '))).toEqual({
+ results: [],
+ searchText: ' ',
+ selectedResult: null,
+ isExactMatch: false,
+ });
+ });
+
+ it('Preforms a basic search, only returning four results', () => {
+ expect(search(undefined, inputChange('18'))).toEqual({
+ results: [
+ 's/18561',
+ 's/18562',
+ 's/18563',
+ 's/18564',
+ ],
+ searchText: '18',
+ selectedResult: null,
+ isExactMatch: false,
+ });
+ });
+
+ it('Selects the first result and sets isExactMatch to true when there is an exact match', () => {
+ expect(search(undefined, inputChange('18561'))).toEqual({
+ results: [
+ 's/18561',
+ ],
+ searchText: '18561',
+ selectedResult: 's/18561',
+ isExactMatch: true,
+ });
+ });
+ });
+
+ describe('SEARCH/CHANGE_SELECTED_RESULT', () => {
+ it('Does nothing when there are no results', () => {
+ const prevState = {
+ results: [],
+ searchText: '',
+ selectedResult: null,
+ isExactMatch: false,
+ };
+
+ const actionPlus = changeSelectedResult(+1);
+ const actionMin = changeSelectedResult(-1);
+
+ deepFreeze([prevState, actionPlus, actionMin]);
+
+ const nextStatePlus = search(prevState, actionPlus);
+ const nextStateMin = search(prevState, actionMin);
+ expect(nextStatePlus).toEqual(prevState);
+ expect(nextStateMin).toEqual(prevState);
+ });
+
+ it('Does nothing when there is an exact match', () => {
+ const prevState = {
+ results: ['s/18561'],
+ searchText: '18561',
+ selectedResult: 's/18561',
+ isExactMatch: true,
+ };
+
+ const actionPlus = changeSelectedResult(+1);
+ const actionMin = changeSelectedResult(-1);
+
+ deepFreeze([prevState, actionPlus, actionMin]);
+
+ const nextStatePlus = search(prevState, actionPlus);
+ const nextStateMin = search(prevState, actionMin);
+
+ expect(nextStatePlus).toEqual(prevState);
+ expect(nextStateMin).toEqual(prevState);
+ });
+
+ it('Switches to the correct selectedResult when no selected result is selected', () => {
+ const prevState = {
+ results: ['s/18561', 's/18562', 's/18563'],
+ searchText: '1856',
+ selectedResult: null,
+ isExactMatch: false,
+ };
+
+ const actionPlus = changeSelectedResult(+1);
+ const actionMin = changeSelectedResult(-1);
+
+ deepFreeze([prevState, actionPlus, actionMin]);
+
+ const nextStatePlus = search(prevState, actionPlus);
+ const nextStateMin = search(prevState, actionMin);
+
+ expect(nextStatePlus).toEqual({
+ ...prevState,
+ selectedResult: 's/18561',
+ });
+ expect(nextStateMin).toEqual({
+ ...prevState,
+ selectedResult: 's/18563',
+ });
+ });
+
+ it('Switches to the correct selectedResult when there is a selected result selected', () => {
+ const prevState = {
+ results: ['s/18561', 's/18562', 's/18563'],
+ searchText: '1856',
+ selectedResult: 's/18562',
+ isExactMatch: false,
+ };
+
+ const actionPlus = changeSelectedResult(+1);
+ const actionMin = changeSelectedResult(-1);
+
+ deepFreeze([prevState, actionPlus, actionMin]);
+
+ const nextStatePlus = search(prevState, actionPlus);
+ const nextStateMin = search(prevState, actionMin);
+
+ expect(nextStatePlus).toEqual({
+ ...prevState,
+ selectedResult: 's/18563',
+ });
+ expect(nextStateMin).toEqual({
+ ...prevState,
+ selectedResult: 's/18561',
+ });
+ });
+
+ it('Properly wraps arround when incrementing', () => {
+ expect(search({
+ results: ['s/18561', 's/18562', 's/18563'],
+ searchText: '1856',
+ selectedResult: 's/18563',
+ isExactMatch: false,
+ }, changeSelectedResult(+1))).toEqual({
+ results: ['s/18561', 's/18562', 's/18563'],
+ searchText: '1856',
+ selectedResult: null,
+ isExactMatch: false,
+ });
+
+ expect(search({
+ results: ['s/18561', 's/18562', 's/18563'],
+ searchText: '1856',
+ selectedResult: null,
+ isExactMatch: false,
+ }, changeSelectedResult(+1))).toEqual({
+ results: ['s/18561', 's/18562', 's/18563'],
+ searchText: '1856',
+ selectedResult: 's/18561',
+ isExactMatch: false,
+ });
+ });
+
+ it('Properly wraps arround when decrementing', () => {
+ expect(search({
+ results: ['s/18561', 's/18562', 's/18563'],
+ searchText: '1856',
+ selectedResult: 's/18561',
+ isExactMatch: false,
+ }, changeSelectedResult(-1))).toEqual({
+ results: ['s/18561', 's/18562', 's/18563'],
+ searchText: '1856',
+ selectedResult: null,
+ isExactMatch: false,
+ });
+
+ expect(search({
+ results: ['s/18561', 's/18562', 's/18563'],
+ searchText: '1856',
+ selectedResult: null,
+ isExactMatch: false,
+ }, changeSelectedResult(-1))).toEqual({
+ results: ['s/18561', 's/18562', 's/18563'],
+ searchText: '1856',
+ selectedResult: 's/18563',
+ isExactMatch: false,
+ });
+ });
+ });
+ });
+});
diff --git a/src/client/react/reducers/view.js b/src/client/react/reducers/view.js
new file mode 100644
index 0000000..301a1cf
--- /dev/null
+++ b/src/client/react/reducers/view.js
@@ -0,0 +1,53 @@
+const schedule = (state = {}, action) => {
+ switch (action.type) {
+ case 'VIEW/FETCH_SCHEDULE_REQUEST':
+ return {
+ ...state,
+ state: 'FETCHING',
+ };
+ case 'VIEW/FETCH_SCHEDULE_SUCCESS':
+ return {
+ ...state,
+ state: 'FINISHED',
+ htmlStr: action.htmlStr,
+ };
+ default:
+ return state;
+ }
+};
+
+const DEFAULT_STATE = {
+ schedules: {},
+};
+
+const view = (state = DEFAULT_STATE, action) => {
+ switch (action.type) {
+ case 'VIEW/FETCH_SCHEDULE_REQUEST':
+ case 'VIEW/FETCH_SCHEDULE_SUCCESS':
+ return {
+ ...state,
+ schedules: {
+ ...state.schedules,
+ [action.user]:
+ state.schedules[action.user]
+ ? {
+ // This user already exists in our state, extend it.
+ ...state.schedules[action.user],
+ [action.week]: schedule(state.schedules[action.user][action.week], action),
+ }
+ : {
+ // This user does not already exist in our state.
+ [action.week]: schedule(undefined, action),
+ },
+ },
+ };
+ default:
+ return state;
+ }
+};
+
+export default view;
+
+export const _test = {
+ DEFAULT_STATE,
+};