From 2ac96b6fd7f96c9ff3498c9e5b028a63c570f336 Mon Sep 17 00:00:00 2001 From: KvNGCzA Date: Tue, 25 Dec 2018 15:56:56 +0100 Subject: [PATCH] feature(SearchPage): enable search functionality on the application - enable search feature througout the website - write tests for new feature and components - fix broken tests [Finishes #161290978] --- mockdata/authors.js | 22 + mockdata/searchResults.js | 495 ++++++++++++++++++ package-lock.json | 277 ++++++++-- package.json | 1 + public/styles/SearchPage.scss | 33 +- public/styles/index.scss | 1 + src/actions/actionTypes.js | 6 + src/actions/authorsActions.js | 32 ++ src/actions/searchActions.js | 41 ++ src/components/SearchPage/SearchPage.js | 377 ++++++++++--- src/components/common/Pagination.js | 38 +- src/components/common/header/Header.js | 60 ++- src/components/common/header/Login.js | 9 +- src/components/common/header/NavBar.js | 3 +- src/components/common/header/UserSettings.js | 11 +- src/components/login/LoginPage.js | 1 - src/reducers/authorReducer.js | 21 + src/reducers/rootReducer.js | 4 + src/reducers/searchReducer.js | 19 + src/store/initialState.js | 9 + src/utils/constructQuery.js | 23 + src/utils/detachOptions.js | 7 + src/utils/paginationHelper.js | 2 +- tests/src/actions/authorsActions.test.js | 66 +++ tests/src/actions/searchActions.test.js | 75 +++ tests/src/components/SearchPage.test.js | 11 - .../components/SearchPage/SearchPage.test.js | 189 +++++++ .../components/common/Header/Header.test.js | 2 + .../components/common/Header/Login.test.js | 4 +- .../src/components/common/Pagination.test.js | 8 +- tests/src/components/login/LoginPage.test.js | 2 + .../components/welcome/WelcomePage.test.js | 2 +- tests/src/reducers/authorReducer.test.js | 25 + tests/src/reducers/searchReducer.test.js | 49 ++ tests/src/utils/constructQuery.test.js | 58 ++ tests/src/utils/detachOptions.test.js | 35 ++ tests/src/utils/paginationHelper.test.js | 2 +- 37 files changed, 1834 insertions(+), 186 deletions(-) create mode 100644 mockdata/searchResults.js create mode 100644 src/actions/authorsActions.js create mode 100644 src/actions/searchActions.js create mode 100644 src/reducers/authorReducer.js create mode 100644 src/reducers/searchReducer.js create mode 100644 src/utils/constructQuery.js create mode 100644 src/utils/detachOptions.js create mode 100644 tests/src/actions/authorsActions.test.js create mode 100644 tests/src/actions/searchActions.test.js delete mode 100644 tests/src/components/SearchPage.test.js create mode 100644 tests/src/components/SearchPage/SearchPage.test.js create mode 100644 tests/src/reducers/authorReducer.test.js create mode 100644 tests/src/reducers/searchReducer.test.js create mode 100644 tests/src/utils/constructQuery.test.js create mode 100644 tests/src/utils/detachOptions.test.js diff --git a/mockdata/authors.js b/mockdata/authors.js index de423e5..29be581 100644 --- a/mockdata/authors.js +++ b/mockdata/authors.js @@ -22,6 +22,28 @@ export default { articlesRead: 0, createdAt: '2018-12-14T13:06:20.151Z', articlesWritten: 5 + }, + { + id: 3, + fullName: 'Akanmu Christopher', + bio: null, + avatarUrl: null, + following: 0, + followers: 0, + articlesRead: 0, + createdAt: '2018-12-14T13:06:20.151Z', + articlesWritten: 5 + }, + { + id: 17, + fullName: 'Augustine Ezinwa', + bio: null, + avatarUrl: null, + following: 0, + followers: 0, + articlesRead: 0, + createdAt: '2018-12-14T13:06:20.151Z', + articlesWritten: 5 } ] }; diff --git a/mockdata/searchResults.js b/mockdata/searchResults.js new file mode 100644 index 0000000..05916d2 --- /dev/null +++ b/mockdata/searchResults.js @@ -0,0 +1,495 @@ +export default { + status: 'success', + totalPages: 1, + currentPage: 1, + totalArticles: 18, + articlesOnPage: 18, + articles: [ + { + id: 18, + articleImage: 'https://bit.ly/2CaG1ce', + title: 'Valinor', + slug: 'team-valinore', + description: 'Team valinor is a simulation team', + body: 'They are handling authors haven product. Good luck, guys.', + readTime: 3600, + category: 'sports', + rating: null, + likes: 0, + dislikes: 0, + status: 'publish', + commentsCount: 0, + createdAt: '2018-12-24T18:11:27.171Z', + updatedAt: '2018-12-24T18:11:27.171Z', + author: { + id: 2, + fullName: 'John Mike', + avatarUrl: 'https://bit.ly/2UT01ax', + roleId: 3, + email: 'johnmike@andela.com', + bio: null, + followers: 0, + following: 0 + } + }, + { + id: 17, + articleImage: 'https://bit.ly/2CaG1ce', + title: 'Jambolani', + slug: 'south-africa-201e', + description: 'Jambolani is the fifa ball', + body: 'Jambolani is the fifa ball used in south africa 2010 world cup', + readTime: 3600, + category: 'sports', + rating: null, + likes: 0, + dislikes: 0, + status: 'publish', + commentsCount: 0, + createdAt: '2018-12-24T18:11:27.171Z', + updatedAt: '2018-12-24T18:11:27.171Z', + author: { + id: 2, + fullName: 'John Mike', + avatarUrl: 'https://bit.ly/2UT01ax', + roleId: 3, + email: 'johnmike@andela.com', + bio: null, + followers: 0, + following: 0 + } + }, + { + id: 16, + articleImage: 'https://bit.ly/2CaG1ce', + title: 'My story at the beach', + slug: 'My-story-at-the-beach-2324232323e', + description: 'This is my story at the beach', + body: 'Once upon a time in Mexico.. there was ...', + readTime: 3600, + category: 'sports', + rating: null, + likes: 0, + dislikes: 0, + status: 'publish', + commentsCount: 0, + createdAt: '2018-12-24T18:11:27.171Z', + updatedAt: '2018-12-24T18:11:27.171Z', + author: { + id: 1, + fullName: 'John Doe', + avatarUrl: 'https://bit.ly/2UT01ax', + roleId: 3, + email: 'johndoe@andela.com', + bio: null, + followers: 0, + following: 0 + } + }, + { + id: 15, + articleImage: 'https://bit.ly/2CaG1ce', + title: 'Valinor', + slug: 'team-valinord', + description: 'Team valinor is a simulation team', + body: 'They are handling authors haven product. Good luck, guys.', + readTime: 3600, + category: 'fashion', + rating: null, + likes: 0, + dislikes: 0, + status: 'publish', + commentsCount: 0, + createdAt: '2018-12-24T18:11:27.171Z', + updatedAt: '2018-12-24T18:11:27.171Z', + author: { + id: 2, + fullName: 'John Mike', + avatarUrl: 'https://bit.ly/2UT01ax', + roleId: 3, + email: 'johnmike@andela.com', + bio: null, + followers: 0, + following: 0 + } + }, + { + id: 14, + articleImage: 'https://bit.ly/2CaG1ce', + title: 'Jambolani', + slug: 'south-africa-201d', + description: 'Jambolani is the fifa ball', + body: 'Jambolani is the fifa ball used in south africa 2010 world cup', + readTime: 3600, + category: 'technology', + rating: null, + likes: 0, + dislikes: 0, + status: 'publish', + commentsCount: 0, + createdAt: '2018-12-24T18:11:27.171Z', + updatedAt: '2018-12-24T18:11:27.171Z', + author: { + id: 2, + fullName: 'John Mike', + avatarUrl: 'https://bit.ly/2UT01ax', + roleId: 3, + email: 'johnmike@andela.com', + bio: null, + followers: 0, + following: 0 + } + }, + { + id: 13, + articleImage: 'https://bit.ly/2CaG1ce', + title: 'My story at the beach', + slug: 'My-story-at-the-beach-2324232323d', + description: 'This is my story at the beach', + body: 'Once upon a time in Mexico.. there was ...', + readTime: 3600, + category: 'technology', + rating: null, + likes: 0, + dislikes: 0, + status: 'publish', + commentsCount: 0, + createdAt: '2018-12-24T18:11:27.171Z', + updatedAt: '2018-12-24T18:11:27.171Z', + author: { + id: 1, + fullName: 'John Doe', + avatarUrl: 'https://bit.ly/2UT01ax', + roleId: 3, + email: 'johndoe@andela.com', + bio: null, + followers: 0, + following: 0 + } + }, + { + id: 12, + articleImage: 'https://bit.ly/2CaG1ce', + title: 'Valinor', + slug: 'team-valinorc', + description: 'Team valinor is a simulation team', + body: 'They are handling authors haven product. Good luck, guys.', + readTime: 3600, + category: 'sports', + rating: null, + likes: 0, + dislikes: 3, + status: 'publish', + commentsCount: 0, + createdAt: '2018-12-24T18:11:27.171Z', + updatedAt: '2018-12-24T18:11:27.171Z', + author: { + id: 2, + fullName: 'John Mike', + avatarUrl: 'https://bit.ly/2UT01ax', + roleId: 3, + email: 'johnmike@andela.com', + bio: null, + followers: 0, + following: 0 + } + }, + { + id: 11, + articleImage: 'https://bit.ly/2CaG1ce', + title: 'Jambolani', + slug: 'south-africa-201c', + description: 'Jambolani is the fifa ball', + body: 'Jambolani is the fifa ball used in south africa 2010 world cup', + readTime: 3600, + category: 'fashion', + rating: null, + likes: 0, + dislikes: 0, + status: 'publish', + commentsCount: 0, + createdAt: '2018-12-24T18:11:27.171Z', + updatedAt: '2018-12-24T18:11:27.171Z', + author: { + id: 2, + fullName: 'John Mike', + avatarUrl: 'https://bit.ly/2UT01ax', + roleId: 3, + email: 'johnmike@andela.com', + bio: null, + followers: 0, + following: 0 + } + }, + { + id: 10, + articleImage: 'https://bit.ly/2CaG1ce', + title: 'My story at the beach', + slug: 'My-story-at-the-beach-2324232323c', + description: 'This is my story at the beach', + body: 'Once upon a time in Mexico.. there was ...', + readTime: 3600, + category: 'sports', + rating: null, + likes: 0, + dislikes: 0, + status: 'publish', + commentsCount: 0, + createdAt: '2018-12-24T18:11:27.171Z', + updatedAt: '2018-12-24T18:11:27.171Z', + author: { + id: 1, + fullName: 'John Doe', + avatarUrl: 'https://bit.ly/2UT01ax', + roleId: 3, + email: 'johndoe@andela.com', + bio: null, + followers: 0, + following: 0 + } + }, + { + id: 9, + articleImage: 'https://bit.ly/2CaG1ce', + title: 'Valinor', + slug: 'team-valinorb', + description: 'Team valinor is a simulation team', + body: 'They are handling authors haven product. Good luck, guys.', + readTime: 3600, + category: 'sports', + rating: null, + likes: 0, + dislikes: 1, + status: 'publish', + commentsCount: 0, + createdAt: '2018-12-24T18:11:27.171Z', + updatedAt: '2018-12-24T18:11:27.171Z', + author: { + id: 2, + fullName: 'John Mike', + avatarUrl: 'https://bit.ly/2UT01ax', + roleId: 3, + email: 'johnmike@andela.com', + bio: null, + followers: 0, + following: 0 + } + }, + { + id: 8, + articleImage: 'https://bit.ly/2CaG1ce', + title: 'Jambolani', + slug: 'south-africa-201b', + description: 'Jambolani is the fifa ball', + body: 'Jambolani is the fifa ball used in south africa 2010 world cup', + readTime: 3600, + category: 'technology', + rating: null, + likes: 0, + dislikes: 0, + status: 'publish', + commentsCount: 0, + createdAt: '2018-12-24T18:11:27.171Z', + updatedAt: '2018-12-24T18:11:27.171Z', + author: { + id: 2, + fullName: 'John Mike', + avatarUrl: 'https://bit.ly/2UT01ax', + roleId: 3, + email: 'johnmike@andela.com', + bio: null, + followers: 0, + following: 0 + } + }, + { + id: 7, + articleImage: 'https://bit.ly/2CaG1ce', + title: 'My story at the beach', + slug: 'My-story-at-the-beach-2324232323b', + description: 'This is my story at the beach', + body: 'Once upon a time in Mexico.. there was ...', + readTime: 3600, + category: 'fashion', + rating: null, + likes: 0, + dislikes: 0, + status: 'publish', + commentsCount: 0, + createdAt: '2018-12-24T18:11:27.171Z', + updatedAt: '2018-12-24T18:11:27.171Z', + author: { + id: 1, + fullName: 'John Doe', + avatarUrl: 'https://bit.ly/2UT01ax', + roleId: 3, + email: 'johndoe@andela.com', + bio: null, + followers: 0, + following: 0 + } + }, + { + id: 6, + articleImage: 'https://bit.ly/2CaG1ce', + title: 'Valinor', + slug: 'team-valinora', + description: 'Team valinor is a simulation team', + body: 'They are handling authors haven product. Good luck, guys.', + readTime: 3600, + category: 'technology', + rating: null, + likes: 0, + dislikes: 0, + status: 'publish', + commentsCount: 0, + createdAt: '2018-12-24T18:11:27.171Z', + updatedAt: '2018-12-24T18:11:27.171Z', + author: { + id: 2, + fullName: 'John Mike', + avatarUrl: 'https://bit.ly/2UT01ax', + roleId: 3, + email: 'johnmike@andela.com', + bio: null, + followers: 0, + following: 0 + } + }, + { + id: 5, + articleImage: 'https://bit.ly/2CaG1ce', + title: 'Jambolani', + slug: 'south-africa-201a', + description: 'Jambolani is the fifa ball', + body: 'Jambolani is the fifa ball used in south africa 2010 world cup', + readTime: 3600, + category: 'fashion', + rating: null, + likes: 1, + dislikes: 1, + status: 'publish', + commentsCount: 0, + createdAt: '2018-12-24T18:11:27.171Z', + updatedAt: '2018-12-24T18:11:27.171Z', + author: { + id: 2, + fullName: 'John Mike', + avatarUrl: 'https://bit.ly/2UT01ax', + roleId: 3, + email: 'johnmike@andela.com', + bio: null, + followers: 0, + following: 0 + } + }, + { + id: 4, + articleImage: 'https://bit.ly/2CaG1ce', + title: 'My story at the beach', + slug: 'My-story-at-the-beach-2324232323a', + description: 'This is my story at the beach', + body: 'Once upon a time in Mexico.. there was ...', + readTime: 3600, + category: 'technology', + rating: null, + likes: 1, + dislikes: 0, + status: 'publish', + commentsCount: 0, + createdAt: '2018-12-24T18:11:27.171Z', + updatedAt: '2018-12-24T18:11:27.171Z', + author: { + id: 1, + fullName: 'John Doe', + avatarUrl: 'https://bit.ly/2UT01ax', + roleId: 3, + email: 'johndoe@andela.com', + bio: null, + followers: 0, + following: 0 + } + }, + { + id: 3, + articleImage: 'https://bit.ly/2CaG1ce', + title: 'Valinor', + slug: 'team-valinor', + description: 'Team valinor is a simulation team', + body: 'They are handling authors haven product. Good luck, guys.', + readTime: 3600, + category: 'fashion', + rating: null, + likes: 1, + dislikes: 0, + status: 'publish', + commentsCount: 0, + createdAt: '2018-12-24T18:11:27.171Z', + updatedAt: '2018-12-24T18:11:27.171Z', + author: { + id: 2, + fullName: 'John Mike', + avatarUrl: 'https://bit.ly/2UT01ax', + roleId: 3, + email: 'johnmike@andela.com', + bio: null, + followers: 0, + following: 0 + } + }, + { + id: 2, + articleImage: 'https://bit.ly/2CaG1ce', + title: 'Jambolani', + slug: 'south-africa-201', + description: 'Jambolani is the fifa ball', + body: 'Jambolani is the fifa ball used in south africa 2010 world cup', + readTime: 3600, + category: 'technology', + rating: null, + likes: 3, + dislikes: 0, + status: 'publish', + commentsCount: 4, + createdAt: '2018-12-24T18:11:27.171Z', + updatedAt: '2018-12-24T18:11:27.171Z', + author: { + id: 2, + fullName: 'John Mike', + avatarUrl: 'https://bit.ly/2UT01ax', + roleId: 3, + email: 'johnmike@andela.com', + bio: null, + followers: 0, + following: 0 + } + }, + { + id: 1, + articleImage: 'https://bit.ly/2CaG1ce', + title: 'My story at the beach', + slug: 'My-story-at-the-beach-2324232323', + description: 'This is my story at the beach', + body: 'Once upon a time in Mexico.. there was ...', + readTime: 3600, + category: 'sports', + rating: null, + likes: 2, + dislikes: 0, + status: 'publish', + commentsCount: 6, + createdAt: '2018-12-24T18:11:27.171Z', + updatedAt: '2018-12-24T18:11:27.171Z', + author: { + id: 1, + fullName: 'John Doe', + avatarUrl: 'https://bit.ly/2UT01ax', + roleId: 3, + email: 'johndoe@andela.com', + bio: null, + followers: 0, + following: 0 + } + } + ] +}; diff --git a/package-lock.json b/package-lock.json index 13172c0..28466fe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -182,7 +182,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.0.0.tgz", "integrity": "sha512-aP/hlLq01DWNEiDg4Jn23i+CXxW/owM4WpDLFUbpjxe4NS3BhLVZQ5i7E0ZrxuQ/vwekIeciyamgB1UIYxxM6A==", - "dev": true, "requires": { "@babel/types": "^7.0.0" } @@ -993,7 +992,6 @@ "version": "7.1.6", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.1.6.tgz", "integrity": "sha512-DMiUzlY9DSjVsOylJssxLHSgj6tWM9PRFJOGW/RaOglVOK9nzTxoOMfTfRQXGUCUQ/HmlG2efwC+XqUEJ5ay4w==", - "dev": true, "requires": { "esutils": "^2.0.2", "lodash": "^4.17.10", @@ -1012,6 +1010,62 @@ "integrity": "sha512-pH4KCsbtBLLe7eqUrw8brcuFO8IZlN36JjdKlOublibVdAIPHCzEnpBWOVUXK5sCf+DpBi8ZtuWtjF0srybdeA==", "dev": true }, + "@emotion/babel-utils": { + "version": "0.6.10", + "resolved": "https://registry.npmjs.org/@emotion/babel-utils/-/babel-utils-0.6.10.tgz", + "integrity": "sha512-/fnkM/LTEp3jKe++T0KyTszVGWNKPNOUJfjNKLO17BzQ6QPxgbg3whayom1Qr2oLFH3V92tDymU+dT5q676uow==", + "requires": { + "@emotion/hash": "^0.6.6", + "@emotion/memoize": "^0.6.6", + "@emotion/serialize": "^0.9.1", + "convert-source-map": "^1.5.1", + "find-root": "^1.1.0", + "source-map": "^0.7.2" + }, + "dependencies": { + "source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==" + } + } + }, + "@emotion/hash": { + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.6.6.tgz", + "integrity": "sha512-ojhgxzUHZ7am3D2jHkMzPpsBAiB005GF5YU4ea+8DNPybMk01JJUM9V9YRlF/GE95tcOm8DxQvWA2jq19bGalQ==" + }, + "@emotion/memoize": { + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.6.6.tgz", + "integrity": "sha512-h4t4jFjtm1YV7UirAFuSuFGyLa+NNxjdkq6DpFLANNQY5rHueFZHVY+8Cu1HYVP6DrheB0kv4m5xPjo7eKT7yQ==" + }, + "@emotion/serialize": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-0.9.1.tgz", + "integrity": "sha512-zTuAFtyPvCctHBEL8KZ5lJuwBanGSutFEncqLn/m9T1a6a93smBStK+bZzcNPgj4QS8Rkw9VTwJGhRIUVO8zsQ==", + "requires": { + "@emotion/hash": "^0.6.6", + "@emotion/memoize": "^0.6.6", + "@emotion/unitless": "^0.6.7", + "@emotion/utils": "^0.8.2" + } + }, + "@emotion/stylis": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.7.1.tgz", + "integrity": "sha512-/SLmSIkN13M//53TtNxgxo57mcJk/UJIDFRKwOiLIBEyBHEcipgR6hNMQ/59Sl4VjCJ0Z/3zeAZyvnSLPG/1HQ==" + }, + "@emotion/unitless": { + "version": "0.6.7", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.6.7.tgz", + "integrity": "sha512-Arj1hncvEVqQ2p7Ega08uHLr1JuRYBuO5cIvcA+WWEQ5+VmkOE3ZXzl04NbQxeQpWX78G7u6MqxKuNX3wvYZxg==" + }, + "@emotion/utils": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-0.8.2.tgz", + "integrity": "sha512-rLu3wcBWH4P5q1CGoSSH/i9hrXs7SlbRLkoq9IGuoPYNGQvDJ3pt/wmOM+XgYjIDRMVIdkUWt0RsfzF50JfnCw==" + }, "@sinonjs/commons": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.3.0.tgz", @@ -1280,8 +1334,7 @@ "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "dev": true + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" }, "accepts": { "version": "1.3.5", @@ -1489,7 +1542,6 @@ "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, "requires": { "sprintf-js": "~1.0.2" } @@ -1907,6 +1959,32 @@ "babel-runtime": "^6.22.0" } }, + "babel-plugin-emotion": { + "version": "9.2.11", + "resolved": "https://registry.npmjs.org/babel-plugin-emotion/-/babel-plugin-emotion-9.2.11.tgz", + "integrity": "sha512-dgCImifnOPPSeXod2znAmgc64NhaaOjGEHROR/M+lmStb3841yK1sgaDYAYMnlvWNz8GnpwIPN0VmNpbWYZ+VQ==", + "requires": { + "@babel/helper-module-imports": "^7.0.0", + "@emotion/babel-utils": "^0.6.4", + "@emotion/hash": "^0.6.2", + "@emotion/memoize": "^0.6.1", + "@emotion/stylis": "^0.7.0", + "babel-plugin-macros": "^2.0.0", + "babel-plugin-syntax-jsx": "^6.18.0", + "convert-source-map": "^1.5.0", + "find-root": "^1.1.0", + "mkdirp": "^0.5.1", + "source-map": "^0.5.7", + "touch": "^2.0.1" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + } + } + }, "babel-plugin-istanbul": { "version": "4.1.6", "resolved": "http://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-4.1.6.tgz", @@ -1925,6 +2003,42 @@ "integrity": "sha1-5h+uBaHKiAGq3uV6bWa4zvr0QWc=", "dev": true }, + "babel-plugin-macros": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-2.4.3.tgz", + "integrity": "sha512-M8cE1Rx0zgfKYBWAS+T6ZVCLGuTKdBI5Rn3fu9q6iVdH0UjaXdmF501/VEYn7kLHCgguhGNk5JBzOn64e2xDEA==", + "requires": { + "cosmiconfig": "^5.0.5", + "resolve": "^1.8.1" + }, + "dependencies": { + "cosmiconfig": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.0.7.tgz", + "integrity": "sha512-PcLqxTKiDmNT6pSpy4N6KtuPwb53W+2tzNvwOZw0WH9N6O0vLIBq0x8aj8Oj75ere4YcGi48bDFCL+3fRJdlNA==", + "requires": { + "import-fresh": "^2.0.0", + "is-directory": "^0.3.1", + "js-yaml": "^3.9.0", + "parse-json": "^4.0.0" + } + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + } + } + }, + "babel-plugin-syntax-jsx": { + "version": "6.18.0", + "resolved": "http://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", + "integrity": "sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY=" + }, "babel-plugin-syntax-object-rest-spread": { "version": "6.13.0", "resolved": "http://registry.npmjs.org/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz", @@ -2597,7 +2711,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", "integrity": "sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ=", - "dev": true, "requires": { "callsites": "^2.0.0" }, @@ -2605,8 +2718,7 @@ "callsites": { "version": "2.0.0", "resolved": "http://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", - "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=", - "dev": true + "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=" } } }, @@ -3285,7 +3397,6 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz", "integrity": "sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==", - "dev": true, "requires": { "safe-buffer": "~5.1.1" } @@ -3379,6 +3490,20 @@ "elliptic": "^6.0.0" } }, + "create-emotion": { + "version": "9.2.12", + "resolved": "https://registry.npmjs.org/create-emotion/-/create-emotion-9.2.12.tgz", + "integrity": "sha512-P57uOF9NL2y98Xrbl2OuiDQUZ30GVmASsv5fbsjF4Hlraip2kyAvMm+2PoYUvFFw03Fhgtxk3RqZSm2/qHL9hA==", + "requires": { + "@emotion/hash": "^0.6.2", + "@emotion/memoize": "^0.6.1", + "@emotion/stylis": "^0.7.0", + "@emotion/unitless": "^0.6.2", + "csstype": "^2.5.2", + "stylis": "^3.5.0", + "stylis-rule-sheet": "^0.0.10" + } + }, "create-hash": { "version": "1.2.0", "resolved": "http://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", @@ -4066,6 +4191,14 @@ "utila": "~0.4" } }, + "dom-helpers": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-3.4.0.tgz", + "integrity": "sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==", + "requires": { + "@babel/runtime": "^7.1.2" + } + }, "dom-serializer": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz", @@ -4256,6 +4389,15 @@ "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=", "dev": true }, + "emotion": { + "version": "9.2.12", + "resolved": "https://registry.npmjs.org/emotion/-/emotion-9.2.12.tgz", + "integrity": "sha512-hcx7jppaI8VoXxIWEhxpDW7I+B4kq9RNzQLmsrF6LY8BGKqe2N+gFAQr0EfuFucFlPs2A9HM4+xNj4NeqEWIOQ==", + "requires": { + "babel-plugin-emotion": "^9.2.11", + "create-emotion": "^9.2.12" + } + }, "encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", @@ -4370,7 +4512,6 @@ "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, "requires": { "is-arrayish": "^0.2.1" } @@ -4813,8 +4954,7 @@ "esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" }, "esquery": { "version": "1.0.1", @@ -4843,8 +4983,7 @@ "esutils": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", - "dev": true + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=" }, "etag": { "version": "1.8.1", @@ -5410,6 +5549,11 @@ "pkg-dir": "^2.0.0" } }, + "find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" + }, "find-up": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", @@ -7102,7 +7246,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", "integrity": "sha1-2BNVwVYS04bGH53dOSLUMEgipUY=", - "dev": true, "requires": { "caller-path": "^2.0.0", "resolve-from": "^3.0.0" @@ -7112,7 +7255,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz", "integrity": "sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ=", - "dev": true, "requires": { "caller-callsite": "^2.0.0" } @@ -7120,8 +7262,7 @@ "resolve-from": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", - "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", - "dev": true + "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=" } } }, @@ -7368,8 +7509,7 @@ "is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", - "dev": true + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" }, "is-binary-path": { "version": "1.0.1", @@ -7478,8 +7618,7 @@ "is-directory": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", - "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=", - "dev": true + "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=" }, "is-dotfile": { "version": "1.0.3", @@ -9092,7 +9231,6 @@ "version": "3.12.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.0.tgz", "integrity": "sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A==", - "dev": true, "requires": { "argparse": "^1.0.7", "esprima": "^4.0.0" @@ -9161,8 +9299,7 @@ "json-parse-better-errors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", - "dev": true + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==" }, "json-schema": { "version": "0.2.3", @@ -9711,6 +9848,11 @@ "p-is-promise": "^1.1.0" } }, + "memoize-one": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-4.0.3.tgz", + "integrity": "sha512-QmpUu4KqDmX0plH4u+tf0riMc1KHE1+lw95cMrLlXQAFOx/xnBtwhZ52XJxd9X2O6kwKBqX32kmhbhlobD0cuw==" + }, "memory-fs": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", @@ -10051,7 +10193,6 @@ "version": "0.5.1", "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "dev": true, "requires": { "minimist": "0.0.8" }, @@ -10059,8 +10200,7 @@ "minimist": { "version": "0.0.8", "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" } } }, @@ -10992,7 +11132,7 @@ }, "path-browserify": { "version": "0.0.0", - "resolved": "http://registry.npmjs.org/path-browserify/-/path-browserify-0.0.0.tgz", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.0.tgz", "integrity": "sha1-oLhwcpquIUAFt9UDLsLLuw+0RRo=", "dev": true }, @@ -11029,8 +11169,7 @@ "path-parse": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", - "dev": true + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" }, "path-to-regexp": { "version": "0.1.7", @@ -11070,8 +11209,7 @@ "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", - "dev": true + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" }, "pify": { "version": "3.0.0", @@ -12527,7 +12665,6 @@ "version": "3.4.1", "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", - "dev": true, "requires": { "performance-now": "^2.1.0" } @@ -12711,6 +12848,14 @@ "prop-types": "^15.6.0" } }, + "react-input-autosize": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/react-input-autosize/-/react-input-autosize-2.2.1.tgz", + "integrity": "sha512-3+K4CD13iE4lQQ2WlF8PuV5htfmTRLH6MDnfndHM6LuBRszuXnuyIfE7nhSKt8AzRBZ50bu0sAhkNMeS5pxQQA==", + "requires": { + "prop-types": "^15.5.8" + } + }, "react-is": { "version": "16.6.3", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.6.3.tgz", @@ -12812,6 +12957,20 @@ "warning": "^4.0.1" } }, + "react-select": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/react-select/-/react-select-2.1.2.tgz", + "integrity": "sha512-+ceiz2KwIeEBxT/PgAXBIGohLXfa9YhkfwFSHMlqpTL55JYvjhgkGoBxoasGcMGeQ49J3RhAKZDD+x6ZHKmj6g==", + "requires": { + "classnames": "^2.2.5", + "emotion": "^9.1.2", + "memoize-one": "^4.0.0", + "prop-types": "^15.6.0", + "raf": "^3.4.0", + "react-input-autosize": "^2.2.1", + "react-transition-group": "^2.2.1" + } + }, "react-tag-input": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/react-tag-input/-/react-tag-input-6.1.0.tgz", @@ -12878,6 +13037,17 @@ "scheduler": "^0.11.2" } }, + "react-transition-group": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.5.2.tgz", + "integrity": "sha512-vwHP++S+f6KL7rg8V1mfs62+MBKtbMeZDR8KiNmD7v98Gs3UPGsDZDahPJH2PVprFW5YHJfh6cbNim3zPndaSQ==", + "requires": { + "dom-helpers": "^3.3.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2", + "react-lifecycles-compat": "^3.0.4" + } + }, "read-pkg": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", @@ -13326,7 +13496,6 @@ "version": "1.8.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.8.1.tgz", "integrity": "sha512-AicPrAC7Qu1JxPCZ9ZgCZlY35QgFnNqc+0LtbRNxnVw4TXvjQ72wnuL9JQcEBgXkI9JM8MsT9kaQoHcpCRJOYA==", - "dev": true, "requires": { "path-parse": "^1.0.5" } @@ -14541,8 +14710,7 @@ "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", - "dev": true + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" }, "sshpk": { "version": "1.15.2", @@ -14831,6 +14999,16 @@ "postcss-selector-parser": "^3.0.0" } }, + "stylis": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-3.5.4.tgz", + "integrity": "sha512-8/3pSmthWM7lsPBKv7NXkzn2Uc9W7NotcwGNpJaa3k7WMM1XDCA4MgT5k/8BIexd5ydZdboXtU90XH9Ec4Bv/Q==" + }, + "stylis-rule-sheet": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stylis-rule-sheet/-/stylis-rule-sheet-0.0.10.tgz", + "integrity": "sha512-nTbZoaqoBnmK+ptANthb10ZRZOGC+EmTLLUxeYIuHNkEKcmKgXX1XWKkUBT2Ac4es3NybooPe0SmvKdhKJZAuw==" + }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -15357,8 +15535,7 @@ "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", - "dev": true + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=" }, "to-object-path": { "version": "0.3.0", @@ -15408,6 +15585,24 @@ "integrity": "sha1-LmhELZ9k7HILjMieZEOsbKqVACk=", "dev": true }, + "touch": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/touch/-/touch-2.0.2.tgz", + "integrity": "sha512-qjNtvsFXTRq7IuMLweVgFxmEuQ6gLbRs2jQxL80TtZ31dEKWYIxRXquij6w6VimyDek5hD3PytljHmEtAs2u0A==", + "requires": { + "nopt": "~1.0.10" + }, + "dependencies": { + "nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=", + "requires": { + "abbrev": "1" + } + } + } + }, "tough-cookie": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", @@ -15464,7 +15659,7 @@ }, "tty-browserify": { "version": "0.0.0", - "resolved": "http://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", + "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=", "dev": true }, @@ -15960,7 +16155,7 @@ }, "vm-browserify": { "version": "0.0.4", - "resolved": "http://registry.npmjs.org/vm-browserify/-/vm-browserify-0.0.4.tgz", + "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-0.0.4.tgz", "integrity": "sha1-XX6kW7755Kb/ZflUOOCofDV9WnM=", "dev": true, "requires": { diff --git a/package.json b/package.json index de8a881..aa243ee 100644 --- a/package.json +++ b/package.json @@ -106,6 +106,7 @@ "react-redux-toastr": "^7.4.5", "react-router": "^4.3.1", "react-router-dom": "^4.3.1", + "react-select": "^2.1.2", "react-tag-input": "^6.1.0", "redux": "^4.0.1", "redux-devtools-extension": "^2.13.7", diff --git a/public/styles/SearchPage.scss b/public/styles/SearchPage.scss index 293d20e..f0521da 100644 --- a/public/styles/SearchPage.scss +++ b/public/styles/SearchPage.scss @@ -11,28 +11,33 @@ text-transform: capitalize; } - select { - -webkit-appearance: none; - background-color: $white-color; - border-color: $filter-border; - -webkit-border-radius: 0; - border-radius: 0; - display: block; - font-size: 0.7em; - margin: 0 0 1em; - padding: 0.9em 0.4em; - text-transform: lowercase; - width: 100%; - } - #authors-options { text-transform: capitalize; } + + .css-10nd86i { + margin: 0 0 1em; + + .css-vj8t7z, .css-2o5izw, .css-15k3avv { + border-radius: 0; + font-size: 0.7em; + border-color: $filter-border; + + .css-1492t68, .css-d8oujb { + display: none; + } + } + + } } #primary { padding-right: 1em; + .no-search-results { + margin: 4em 0 0; + } + .search-params { margin: 3em 0 1.5em; diff --git a/public/styles/index.scss b/public/styles/index.scss index 1138296..fa857eb 100644 --- a/public/styles/index.scss +++ b/public/styles/index.scss @@ -37,6 +37,7 @@ body { position: relative; .site-content { + min-height: 100vh; margin-top: 3.2em; overflow: hidden; diff --git a/src/actions/actionTypes.js b/src/actions/actionTypes.js index bd7ccad..79433fe 100644 --- a/src/actions/actionTypes.js +++ b/src/actions/actionTypes.js @@ -16,5 +16,11 @@ export const FETCH_CATEGORY_TITLES_FAILURE = 'FETCH_CATEGORY_TITLES_FAILURE'; export const FETCH_TAG_TITLES_SUCCESS = 'FETCH_TAG_TITLES_SUCCESS'; export const FETCH_TAG_TITLES_FAILURE = 'FETCH_TAG_TITLES_FAILURE'; +export const FETCH_AUTHORS_SUCCESS = 'FETCH_AUTHORS_SUCCESS'; +export const FETCH_AUTHORS_FAILURE = 'FETCH_AUTHORS_FAILURE'; + export const POST_ARTICLE_SUCCESS = 'POST_ARTICLE_SUCCESS'; export const POST_ARTICLE_FAILURE = 'POST_ARTICLE_FAILURE'; + +export const SEARCH_RESULTS_SUCCESS = 'SEARCH_RESULTS_SUCCESS'; +export const SEARCH_RESULTS_FAILURE = 'SEARCH_RESULTS_FAILURE'; diff --git a/src/actions/authorsActions.js b/src/actions/authorsActions.js new file mode 100644 index 0000000..39c5198 --- /dev/null +++ b/src/actions/authorsActions.js @@ -0,0 +1,32 @@ +import { + FETCH_AUTHORS_SUCCESS, + FETCH_AUTHORS_FAILURE, +} from './actionTypes'; + +import { globalFailure } from './globalActions'; + +export const requestAuthorsSuccess = results => ({ + type: FETCH_AUTHORS_SUCCESS, + results, + errors: {} +}); + +export const requestAuthorsFailure = errors => ({ + type: FETCH_AUTHORS_FAILURE, + errors, + results: [] +}); + +const fetchAuthors = () => dispatch => fetch(`${process.env.API_BASE_URL}/users/authors`) + .then( + res => res.json(), + error => dispatch(globalFailure(error)) + ) + .then((authors) => { + if (authors.status === 'failure') { + return dispatch(requestAuthorsFailure(authors.errors.message)); + } + return dispatch(requestAuthorsSuccess(authors.authors)); + }); + +export default fetchAuthors; diff --git a/src/actions/searchActions.js b/src/actions/searchActions.js new file mode 100644 index 0000000..309ec37 --- /dev/null +++ b/src/actions/searchActions.js @@ -0,0 +1,41 @@ +import { toastr } from 'react-redux-toastr'; + +import { globalFailure, globalLoading } from './globalActions'; +import { SEARCH_RESULTS_SUCCESS, SEARCH_RESULTS_FAILURE } from './actionTypes'; +import requestOptions from '../utils/requestOptions'; + +export const searchResultSuccess = (results, query) => ({ + type: SEARCH_RESULTS_SUCCESS, + results, + errors: '', + query +}); + +export const searchResultFailure = (errors, query) => ({ + type: SEARCH_RESULTS_FAILURE, + results: {}, + errors, + query +}); + +const searchAction = searchParams => (dispatch) => { + dispatch(globalLoading(true)); + return fetch(`${process.env.API_BASE_URL}/articles/category/${searchParams}`, requestOptions(null, 'GET', null)) + .then( + res => res.json(), + (error) => { + dispatch(globalFailure(['An error has occured', error])); + toastr.error('An error has occured, please try again!'); + } + ) + .then((response) => { + if (response.errors) { + dispatch(globalLoading(false)); + return dispatch(searchResultFailure(response.errors.message, searchParams)); + } + dispatch(globalLoading(false)); + return dispatch(searchResultSuccess(response, searchParams)); + }); +}; + +export default searchAction; diff --git a/src/components/SearchPage/SearchPage.js b/src/components/SearchPage/SearchPage.js index fe408c5..166a6da 100644 --- a/src/components/SearchPage/SearchPage.js +++ b/src/components/SearchPage/SearchPage.js @@ -1,101 +1,303 @@ -import React, { Component } from 'react'; +import React, { Component, Fragment } from 'react'; import { Link } from 'react-router-dom'; +import Select from 'react-select'; +import makeAnimated from 'react-select/lib/animated'; +import queryString from 'query-string'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; + import Pagination from '../common/Pagination'; -import mockArticles from '../../../mockdata/articles'; -import mockAuthors from '../../../mockdata/authors'; -import mockTags from '../../../mockdata/tags'; -import mockCategories from '../../../mockdata/categories'; - -const { articles } = mockArticles; -const { authors } = mockAuthors; -const { categories } = mockCategories; -const { rows } = mockTags.tags; - -export const Filters = () => ( -
- - category: - - - authors: - - - tags: - +import constructQuery from '../../utils/constructQuery'; +import detachOptions from '../../utils/detachOptions'; +import { fetchCategoryTitles } from '../../actions/categoryActions'; +import { fetchTagTitles } from '../../actions/tagActions'; +import fetchAuthors from '../../actions/authorsActions'; +import searchActions from '../../actions/searchActions'; + + +export const Filters = (props) => { + const { + handleCategoryName, + handleAuthorName, + handleTagName, + categories, + tags, + authors + } = props; + const tagOptions = detachOptions(tags, 'tagName'); + const categoryOptions = detachOptions(categories, 'categoryName'); + let authorOptions; + if (authors.length > 1) { + authorOptions = detachOptions(authors, 'fullName'); + } else { + authorOptions = []; + } + return ( +
-
-); + category: + -class SearchPage extends Component { + tags: + +
+
- {`${mockArticles.totalArticles} search results`} + {numberOfArticles === undefined ? 'No search results' : `${numberOfArticles} search results`}
Results Per Page | - 10 - | - 50 - | - 100 - | + { + limitArr.map(currentLimit => ( + + {currentLimit} + | + + )) + }
- +
- { - articles.map((article, index) => ( -
-
{article.title}
-

{article.body}

-
- )) - } + {body}
-

filter

- +
@@ -103,4 +305,43 @@ class SearchPage extends Component { } } -export default SearchPage; +Filters.propTypes = { + handleCategoryName: PropTypes.func.isRequired, + handleAuthorName: PropTypes.func.isRequired, + handleTagName: PropTypes.func.isRequired, + categories: PropTypes.array.isRequired, + tags: PropTypes.array.isRequired, + authors: PropTypes.array.isRequired, +}; + +SearchPage.propTypes = { + location: PropTypes.object.isRequired, + getCategories: PropTypes.func.isRequired, + getTags: PropTypes.func.isRequired, + getAuthors: PropTypes.func.isRequired, + categories: PropTypes.array.isRequired, + tags: PropTypes.array.isRequired, + authors: PropTypes.array.isRequired, + runSearch: PropTypes.func.isRequired, + searchResults: PropTypes.object.isRequired, + previousQuery: PropTypes.string.isRequired, + errors: PropTypes.string.isRequired +}; + +const mapStateToProps = state => ({ + categories: state.categoryTitles, + tags: state.tagTitles, + authors: state.authors.results, + searchResults: state.searchResults.results, + errors: state.searchResults.errors, + previousQuery: state.searchResults.query, +}); + +const mapDispatchToProps = dispatch => ({ + runSearch: searchTerm => dispatch(searchActions(searchTerm)), + getCategories: () => dispatch(fetchCategoryTitles()), + getTags: () => dispatch(fetchTagTitles()), + getAuthors: () => dispatch(fetchAuthors()) +}); + +export default connect(mapStateToProps, mapDispatchToProps)(SearchPage); diff --git a/src/components/common/Pagination.js b/src/components/common/Pagination.js index be8c630..3b0b0da 100644 --- a/src/components/common/Pagination.js +++ b/src/components/common/Pagination.js @@ -4,12 +4,16 @@ import PropTypes from 'prop-types'; import paginationHelper from '../../utils/paginationHelper'; export const Buttons = (props) => { - const { pages, setActive, currentPage } = props; + const { + pages, + setActive, + currentPage, + } = props; const range = paginationHelper(pages, currentPage); return ( range.map((num, index) => (
  • - +
  • )) ); @@ -26,20 +30,32 @@ class Pagination extends Component { this.decrementPage = this.decrementPage.bind(this); } + componentWillUpdate() { + const { pageNumber } = this.props; + const { currentPage } = this.state; + if (pageNumber !== currentPage) { + this.setActive({ target: { value: pageNumber } }); + } + } + setActive(e) { - return this.setState({ currentPage: Number(e.target.value) }); + const { custom } = this.props; + this.setState({ currentPage: Number(e.target.value) }); + return custom(Number(e.target.value)); } incrementPage() { - return this.setState(prevState => ({ - currentPage: prevState.currentPage + 1 - })); + const { custom } = this.props; + const { currentPage } = this.state; + this.setState(prevState => ({ currentPage: prevState.currentPage + 1 })); + return custom(currentPage + 1); } decrementPage() { - return this.setState(prevState => ({ - currentPage: prevState.currentPage - 1 - })); + const { custom } = this.props; + const { currentPage } = this.state; + this.setState(prevState => ({ currentPage: prevState.currentPage - 1 })); + return custom(currentPage - 1); } render() { @@ -58,7 +74,9 @@ class Pagination extends Component { } Pagination.propTypes = { - pages: PropTypes.number.isRequired + pages: PropTypes.number.isRequired, + pageNumber: PropTypes.number.isRequired, + custom: PropTypes.func.isRequired }; export default Pagination; diff --git a/src/components/common/header/Header.js b/src/components/common/header/Header.js index 3c4c27b..7013e1c 100644 --- a/src/components/common/header/Header.js +++ b/src/components/common/header/Header.js @@ -1,29 +1,45 @@ -import React from 'react'; +import React, { Component } from 'react'; import { NavLink } from 'react-router-dom'; import logo from '../../../../public/images/site-logo.png'; import '../../../../public/js/NavBar'; import Navbar from './NavBar'; -const Header = () => ( -
    - -
    -); +class Header extends Component { + constructor(props) { + super(props); + this.state = {}; + this.handleSearchSubmit = this.handleSearchSubmit.bind(this); + } + + handleSearchSubmit(e) { + e.preventDefault(); + const seacrhTerm = e.target.searchInput.value; + window.location = `/search?search=${seacrhTerm}`; + } + + render() { + return ( +
    + +
    + ); + } +} export default Header; diff --git a/src/components/common/header/Login.js b/src/components/common/header/Login.js index 2c2b7cc..4b7a660 100644 --- a/src/components/common/header/Login.js +++ b/src/components/common/header/Login.js @@ -27,7 +27,7 @@ export class Login extends Component { } render() { - const { url } = this.props; + const { url, handleSearchSubmit } = this.props; return ( @@ -43,8 +43,8 @@ export class Login extends Component {
    -
    - + +
    @@ -132,7 +132,8 @@ export class Login extends Component { Login.propTypes = { url: PropTypes.string.isRequired, - logOutAction: PropTypes.func.isRequired + logOutAction: PropTypes.func.isRequired, + handleSearchSubmit: PropTypes.func.isRequired }; const mapDispatchToProps = dispatch => ({ diff --git a/src/components/common/header/NavBar.js b/src/components/common/header/NavBar.js index 0c79120..ccbf763 100644 --- a/src/components/common/header/NavBar.js +++ b/src/components/common/header/NavBar.js @@ -11,6 +11,7 @@ class NavBar extends Component { render() { const { categories } = this.state; + const { handleSearchSubmit } = this.props; return ( ); } diff --git a/src/components/common/header/UserSettings.js b/src/components/common/header/UserSettings.js index 74bdb30..a45f351 100644 --- a/src/components/common/header/UserSettings.js +++ b/src/components/common/header/UserSettings.js @@ -16,7 +16,7 @@ export class UserSettings extends Component { } render() { - const { isLoggedIn } = this.props; + const { isLoggedIn, handleSearchSubmit } = this.props; if (!isLoggedIn) { return (
      @@ -31,8 +31,8 @@ export class UserSettings extends Component {
      -
      - + +
      @@ -59,12 +59,13 @@ export class UserSettings extends Component {
    ); } - return ; + return ; } } UserSettings.propTypes = { - isLoggedIn: PropTypes.bool.isRequired + isLoggedIn: PropTypes.bool.isRequired, + handleSearchSubmit: PropTypes.func.isRequired, }; const mapStateToProps = state => ({ isLoggedIn: state.global.isLoggedIn }); diff --git a/src/components/login/LoginPage.js b/src/components/login/LoginPage.js index ae6213f..cd77750 100644 --- a/src/components/login/LoginPage.js +++ b/src/components/login/LoginPage.js @@ -127,7 +127,6 @@ LoginPage.propTypes = { }; LoginPage.defaultProps = { - // emailError: [], sendEmail: () => {} }; diff --git a/src/reducers/authorReducer.js b/src/reducers/authorReducer.js new file mode 100644 index 0000000..615a786 --- /dev/null +++ b/src/reducers/authorReducer.js @@ -0,0 +1,21 @@ +import { + FETCH_AUTHORS_SUCCESS, + FETCH_AUTHORS_FAILURE +} from '../actions/actionTypes'; + +import initialState from '../store/initialState'; + +const { authors } = initialState; + +const authorReducer = (state = authors, action) => { + const { type } = action; + switch (type) { + case FETCH_AUTHORS_SUCCESS: + case FETCH_AUTHORS_FAILURE: + return { results: action.results, errors: action.errors }; + default: + return state; + } +}; + +export default authorReducer; diff --git a/src/reducers/rootReducer.js b/src/reducers/rootReducer.js index 76dac82..bd63588 100644 --- a/src/reducers/rootReducer.js +++ b/src/reducers/rootReducer.js @@ -8,6 +8,8 @@ import popularPostsReducer from './popularPostsReducer'; import categoryReducer from './categoryReducer'; import tagReducer from './tagReducer'; import postArticleReducer from './postArticleReducer'; +import searchReducer from './searchReducer'; +import authorReducer from './authorReducer'; const rootReducer = combineReducers({ global, @@ -15,9 +17,11 @@ const rootReducer = combineReducers({ articlesByCategory: articlesReducer, popularArticles: popularPostsReducer, article: singleArticleReducer, + authors: authorReducer, categoryTitles: categoryReducer, tagTitles: tagReducer, postArticle: postArticleReducer, + searchResults: searchReducer }); export default rootReducer; diff --git a/src/reducers/searchReducer.js b/src/reducers/searchReducer.js new file mode 100644 index 0000000..5535253 --- /dev/null +++ b/src/reducers/searchReducer.js @@ -0,0 +1,19 @@ +import initialState from '../store/initialState'; +import { + SEARCH_RESULTS_SUCCESS, + SEARCH_RESULTS_FAILURE +} from '../actions/actionTypes'; + +const { searchResults } = initialState; +const searchReducer = (state = searchResults, action) => { + const { type } = action; + switch (type) { + case SEARCH_RESULTS_SUCCESS: + case SEARCH_RESULTS_FAILURE: + return { results: action.results, errors: action.errors, query: action.query }; + default: + return state; + } +}; + +export default searchReducer; diff --git a/src/store/initialState.js b/src/store/initialState.js index b1e7614..b825e63 100644 --- a/src/store/initialState.js +++ b/src/store/initialState.js @@ -17,5 +17,14 @@ export default { tagTitles: [], postArticle: { errors: {} + }, + authors: { + results: [], + errors: {}, + }, + searchResults: { + results: {}, + query: 'all?limit=10&page=1', + errors: '' } }; diff --git a/src/utils/constructQuery.js b/src/utils/constructQuery.js new file mode 100644 index 0000000..7dd8efe --- /dev/null +++ b/src/utils/constructQuery.js @@ -0,0 +1,23 @@ +export default (category, searchTerm, limit, page, authors, tags) => { + const query = []; + if (searchTerm !== '') { + query.push(`search=${searchTerm}`); + } + query.push(`limit=${limit}`); + query.push(`page=${page}`); + if (tags && tags.length > 0) { + if (tags.length < 2) { + query.push(`tag=${tags[0]}`); + } else { + query.push(`tag=${tags.join(' ')}`); + } + } + if (authors && authors.length > 0) { + if (authors.length < 2) { + query.push(`author=${authors[0]}`); + } else { + query.push(`author=${authors.join(' ')}`); + } + } + return `${category}?${query.join('&')}`; +}; diff --git a/src/utils/detachOptions.js b/src/utils/detachOptions.js new file mode 100644 index 0000000..db052e7 --- /dev/null +++ b/src/utils/detachOptions.js @@ -0,0 +1,7 @@ +export default (information, key) => { + if (key === 'categoryName') { + const result = information.map(current => ({ value: current[key], label: current[key] })); + return [{ value: 'all', label: 'all' }, ...result]; + } + return information.map(current => ({ value: current.id, label: current[key] })); +}; diff --git a/src/utils/paginationHelper.js b/src/utils/paginationHelper.js index 74fcdfa..6ad7102 100644 --- a/src/utils/paginationHelper.js +++ b/src/utils/paginationHelper.js @@ -1,7 +1,7 @@ const paginationHelper = (pages, currentPage) => { const limit = 7; if (currentPage > pages) { - return 'error'; + return [1]; } const result = []; if (pages < limit) { diff --git a/tests/src/actions/authorsActions.test.js b/tests/src/actions/authorsActions.test.js new file mode 100644 index 0000000..e737439 --- /dev/null +++ b/tests/src/actions/authorsActions.test.js @@ -0,0 +1,66 @@ +import configureMockStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; +import fetchMock from 'fetch-mock'; + +import fetchAuthors, { requestAuthorsFailure, requestAuthorsSuccess } from '../../../src/actions/authorsActions'; + +const middlewares = [thunk]; +const mockStore = configureMockStore(middlewares); + +describe('AUTHORS ACTIONS', () => { + it('AUHTORS FAILURE', () => { + const expectedActions = [ + { + type: 'FETCH_AUTHORS_FAILURE', results: [], errors: 'no authors on authors haven' + } + ]; + const store = mockStore({ + authors: { + results: [], + errors: {}, + } + }); + + store.dispatch(requestAuthorsFailure('no authors on authors haven')); + expect(store.getActions()).toEqual(expectedActions); + }); + + it('AUHTORS SUCCESS', () => { + const expectedActions = [ + { + type: 'FETCH_AUTHORS_SUCCESS', results: { results: 'all authors on authors haven' }, errors: {} + } + ]; + const store = mockStore({ + authors: { + results: [], + errors: {}, + } + }); + + store.dispatch(requestAuthorsSuccess({ results: 'all authors on authors haven' })); + expect(store.getActions()).toEqual(expectedActions); + }); + + + it('AUTHORS API CALL', () => { + const initialState = { + authors: { + results: [], + errors: {}, + } + }; + const store = mockStore(initialState); + const expectedActions = [ + { + type: 'FETCH_AUTHORS_FAILURE', results: [], errors: 'no authors' + } + ]; + + fetchMock.get(`${process.env.API_BASE_URL}/users/authors`, { status: 'failure', errors: { message: 'no authors' } }); + return store.dispatch(fetchAuthors()) + .then(() => { + expect(store.getActions()).toEqual(expectedActions); + }); + }); +}); diff --git a/tests/src/actions/searchActions.test.js b/tests/src/actions/searchActions.test.js new file mode 100644 index 0000000..581dc32 --- /dev/null +++ b/tests/src/actions/searchActions.test.js @@ -0,0 +1,75 @@ +import configureMockStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; +import fetchMock from 'fetch-mock'; + +import searchAction, { searchResultFailure, searchResultSuccess } from '../../../src/actions/searchActions'; + +const middlewares = [thunk]; +const mockStore = configureMockStore(middlewares); + +describe('SEARCH ACTIONS', () => { + it('SEARCH FAILURE', () => { + const expectedActions = [ + { + type: 'SEARCH_RESULTS_FAILURE', results: {}, errors: 'no authors on authors haven', query: 'fashion?limit=10' + } + ]; + const store = mockStore({ + searchResults: { + results: [], + errors: {}, + query: '' + } + }); + + store.dispatch(searchResultFailure('no authors on authors haven', 'fashion?limit=10')); + expect(store.getActions()).toEqual(expectedActions); + }); + + it('AUHTORS SUCCESS', () => { + const expectedActions = [ + { + type: 'SEARCH_RESULTS_SUCCESS', results: 'all authors on authors haven', errors: '', query: 'fashion?limit=10' + } + ]; + const store = mockStore({ + searchResults: { + results: [], + errors: {}, + query: '' + } + }); + + store.dispatch(searchResultSuccess('all authors on authors haven', 'fashion?limit=10')); + expect(store.getActions()).toEqual(expectedActions); + }); + + + it('SEARCH ACTION API CALL', () => { + const initialState = { + searchResults: { + results: [], + errors: {}, + query: '' + } + }; + const store = mockStore(initialState); + const expectedActions = [ + { + isLoading: true, type: 'TRIGGER_LOADING' + }, + { + isLoading: false, type: 'TRIGGER_LOADING' + }, + { + errors: 'no authors', query: 'fashion?limit=10', results: {}, type: 'SEARCH_RESULTS_FAILURE' + } + ]; + + fetchMock.get(`${process.env.API_BASE_URL}/articles/category/fashion?limit=10`, { status: 'failure', errors: { message: 'no authors' } }); + return store.dispatch(searchAction('fashion?limit=10')) + .then(() => { + expect(store.getActions()).toEqual(expectedActions); + }); + }); +}); diff --git a/tests/src/components/SearchPage.test.js b/tests/src/components/SearchPage.test.js deleted file mode 100644 index cf6d4f4..0000000 --- a/tests/src/components/SearchPage.test.js +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react'; -import { shallow } from 'enzyme'; - -import SearchPage, { Filters } from '../../../src/components/SearchPage/SearchPage'; - -test('Test search page', () => { - const firstComponent = shallow(); - const secondComponent = shallow(); - expect(firstComponent).toMatchSnapshot(); - expect(secondComponent).toMatchSnapshot(); -}); diff --git a/tests/src/components/SearchPage/SearchPage.test.js b/tests/src/components/SearchPage/SearchPage.test.js new file mode 100644 index 0000000..304b6b0 --- /dev/null +++ b/tests/src/components/SearchPage/SearchPage.test.js @@ -0,0 +1,189 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import configureMockStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; +import fetchMock from 'fetch-mock'; + +import DefaultSearchPage, { SearchPage, Filters } from '../../../../src/components/SearchPage/SearchPage'; +import mockAuthors from '../../../../mockdata/authors'; +import mockCategories from '../../../../mockdata/categories'; +import mockTags from '../../../../mockdata/tags'; +import mockSearch from '../../../../mockdata/searchResults'; + +const middlewares = [thunk]; +const mockStore = configureMockStore(middlewares); +const { authors } = mockAuthors; +const { categories } = mockCategories; +const { tags } = mockTags; + +describe('', () => { + let component, store; + const location = { + search: '?search=boys&tag=1%202%203&author=4%205%206&category=fashion' + }; + + beforeEach(() => { + const initialState = { + global: { + isLoading: false, + isLoggedIn: false + }, + categoryTitles: categories, + authors: { + results: authors + }, + tagTitles: tags.rows, + searchResults: { + results: mockSearch, + errors: '', + query: 'all?limit=10&page=1', + } + }; + store = mockStore(initialState); + component = shallow( + + ); + }); + + it('Test search page', () => { + expect(component).toMatchSnapshot(); + }); + + it('should run getCategories()', () => { + fetchMock.get(`${process.env.API_BASE_URL}/articles/categories`, { categories }); + const prop = component.props(); + prop.getCategories(); + const actions = store.getActions(); + expect(actions).toEqual([]); + }); + + it('should run getTags()', () => { + fetchMock.get(`${process.env.API_BASE_URL}/articles/tags`, { tags: tags.rows }); + const prop = component.props(); + prop.getTags(); + const actions = store.getActions(); + expect(actions).toEqual([]); + }); + + it('should run getAuthors()', () => { + fetchMock.get(`${process.env.API_BASE_URL}/users/authors`, { authors }); + const prop = component.props(); + prop.getAuthors(); + const actions = store.getActions(); + expect(actions).toEqual([]); + }); + + it('should run runSearch()', () => { + fetchMock.get(`${process.env.API_BASE_URL}/articles/category/fashion?author=1`, { ...mockSearch }); + const prop = component.props(); + prop.runSearch('fashion?author=1'); + const actions = store.getActions(); + expect(actions).toEqual([{ type: 'TRIGGER_LOADING', isLoading: true }]); + }); + + it('should run runSearch() and return no search results', () => { + fetchMock.get(`${process.env.API_BASE_URL}/articles/category/fashion?author=2`, { errors: { message: 'no articles found' } }); + const prop = component.props(); + prop.runSearch('fashion?author=2'); + const actions = store.getActions(); + expect(actions).toEqual([{ type: 'TRIGGER_LOADING', isLoading: true }]); + }); + + it('should render filters component', () => { + const secondComponent = shallow( + + ); + const thirdComponent = shallow( + + ); + const fourthComponent = shallow( + + ); + expect(secondComponent).toMatchSnapshot(); + expect(thirdComponent).toMatchSnapshot(); + expect(fourthComponent).toMatchSnapshot(); + const instance = fourthComponent.instance(); + + expect(instance.state.searchTerm).toBe('boys'); + + instance.handleSearchSubmit({ target: { searchTerm: { value: 'girls' } }, persist: jest.fn(), preventDefault: jest.fn() }); + expect(instance.state.searchTerm).toBe('girls'); + + instance.handlePageLimit({ target: { textContent: '100' } }); + expect(instance.state.limit).toBe(100); + + instance.handleTagName([{ value: 1, label: 'football' }]); + expect(instance.state.tagIds).toEqual([1]); + + instance.handleAuthorName([{ value: 1, label: 'football' }]); + expect(instance.state.authorsId).toEqual([1]); + + instance.handleCategoryName({ value: 'football', label: 'football' }); + expect(instance.state.categoryName).toBe('football'); + + instance.handelSearchTerm({ target: { value: 'football' } }); + expect(instance.state.searchTerm).toBe('football'); + + instance.getPageNumber(3); + expect(instance.state.pageNumber).toBe(3); + + instance.setSearchParam('categoryName', 'farmers'); + expect(instance.state.categoryName).toBe('farmers'); + + instance.componentDidMount(); + expect(instance.state.categoryName).toBe('fashion'); + + const fifthComponent = shallow( + + ); + expect(fifthComponent).toMatchSnapshot(); + }); +}); diff --git a/tests/src/components/common/Header/Header.test.js b/tests/src/components/common/Header/Header.test.js index 04438c7..b416f80 100644 --- a/tests/src/components/common/Header/Header.test.js +++ b/tests/src/components/common/Header/Header.test.js @@ -6,4 +6,6 @@ import Header from '../../../../../src/components/common/header/Header'; test('Header snapshot test', () => { const component = shallow(
    ); expect(component).toMatchSnapshot(); + const instance = component.instance(); + instance.handleSearchSubmit({ target: { searchInput: { value: 'jalit' } }, preventDefault: jest.fn() }); }); diff --git a/tests/src/components/common/Header/Login.test.js b/tests/src/components/common/Header/Login.test.js index 7bd53ce..902a108 100644 --- a/tests/src/components/common/Header/Login.test.js +++ b/tests/src/components/common/Header/Login.test.js @@ -9,7 +9,7 @@ const middlewares = [thunk]; const mockStore = configureMockStore(middlewares); test('TEST login component', () => { - const component = shallow(); + const component = shallow(); expect(component).toMatchSnapshot(); const instance = component.instance(); instance.logOutUser(); @@ -25,7 +25,7 @@ describe('', () => { } }; store = mockStore(initialState); - component = shallow(); + component = shallow(); }); it('certify that isLoggedIn is false', () => { diff --git a/tests/src/components/common/Pagination.test.js b/tests/src/components/common/Pagination.test.js index f9a68c2..b358627 100644 --- a/tests/src/components/common/Pagination.test.js +++ b/tests/src/components/common/Pagination.test.js @@ -4,7 +4,7 @@ import { shallow, mount } from 'enzyme'; import Pagination, { Buttons } from '../../../../src/components/common/Pagination'; test('should test pagination and button components', () => { - const componentOne = mount(); + const componentOne = mount(); const componentTwo = shallow(); expect(componentOne).toMatchSnapshot(); @@ -12,13 +12,13 @@ test('should test pagination and button components', () => { const instance = componentOne.instance(); instance.incrementPage(); - expect(instance.state.currentPage).toBe(2); + expect(instance.state.currentPage).toBe(5); instance.decrementPage(); - expect(instance.state.currentPage).toBe(1); + expect(instance.state.currentPage).toBe(4); instance.setActive({ target: { value: 2 } }); - expect(instance.state.currentPage).toBe(2); + expect(instance.state.currentPage).toBe(5); componentOne.find('button').at(5).simulate('click'); expect(instance.state.currentPage).toBe(5); diff --git a/tests/src/components/login/LoginPage.test.js b/tests/src/components/login/LoginPage.test.js index 28329c9..fbd61ca 100644 --- a/tests/src/components/login/LoginPage.test.js +++ b/tests/src/components/login/LoginPage.test.js @@ -39,6 +39,8 @@ describe('should test LoginPage components', () => { const instance = secondComponent.instance(); instance.responseGoogle({ profileObj }); instance.responseFacebook({ userFacebookInformation }); + instance.handleSubmit({ preventDefault: jest.fn() }); + instance.handleInputChange({ preventDefault: jest.fn(), target: { name: 'Chris', value: 'Chris' } }); }); describe('Login snapshot test', () => { diff --git a/tests/src/components/welcome/WelcomePage.test.js b/tests/src/components/welcome/WelcomePage.test.js index e1999e2..046596f 100644 --- a/tests/src/components/welcome/WelcomePage.test.js +++ b/tests/src/components/welcome/WelcomePage.test.js @@ -4,6 +4,6 @@ import { shallow } from 'enzyme'; import { Welcome } from '../../../../src/components/welcome/WelcomePage'; test('Welcome page snapshot test', () => { - const component = shallow(); + const component = shallow(); expect(component).toMatchSnapshot(); }); diff --git a/tests/src/reducers/authorReducer.test.js b/tests/src/reducers/authorReducer.test.js new file mode 100644 index 0000000..6b85620 --- /dev/null +++ b/tests/src/reducers/authorReducer.test.js @@ -0,0 +1,25 @@ +import authorReducer from '../../../src/reducers/authorReducer'; +import { + FETCH_AUTHORS_SUCCESS +} from '../../../src/actions/actionTypes'; + +const initialState = { + results: [], + errors: {}, +}; + +describe('TEST AUTHORS REDUCER', () => { + it('should return isLoading false, isLoggedIn true', () => { + expect(authorReducer( + initialState, + { + type: FETCH_AUTHORS_SUCCESS, + results: { authors: [] }, + errors: '' + } + )).toEqual({ + results: { authors: [] }, + errors: '' + }); + }); +}); diff --git a/tests/src/reducers/searchReducer.test.js b/tests/src/reducers/searchReducer.test.js new file mode 100644 index 0000000..6b2c508 --- /dev/null +++ b/tests/src/reducers/searchReducer.test.js @@ -0,0 +1,49 @@ +import searchReducer from '../../../src/reducers/searchReducer'; +import { + SEARCH_RESULTS_SUCCESS, + SEARCH_RESULTS_FAILURE +} from '../../../src/actions/actionTypes'; + +const initialState = { + results: {}, + query: 'all?limit=10&page=1', + errors: '' +}; + +describe('searchReducer', () => { + it('should return the initial state', () => { + expect(searchReducer(initialState, {})).toEqual(initialState); + }); + + it('should return isLoading false, isLoggedIn true', () => { + expect(searchReducer( + initialState, + { + type: SEARCH_RESULTS_SUCCESS, + results: { articles: [] }, + query: 'all?limit=10&page=1', + errors: '' + } + )).toEqual({ + results: { articles: [] }, + query: 'all?limit=10&page=1', + errors: '' + }); + }); + + it('should return isLoading false, isLoggedIn false', () => { + expect(searchReducer( + initialState, + { + type: SEARCH_RESULTS_FAILURE, + results: {}, + query: 'all?limit=10&page=1', + errors: 'no results' + } + )).toEqual({ + results: {}, + query: 'all?limit=10&page=1', + errors: 'no results' + }); + }); +}); diff --git a/tests/src/utils/constructQuery.test.js b/tests/src/utils/constructQuery.test.js new file mode 100644 index 0000000..af292ed --- /dev/null +++ b/tests/src/utils/constructQuery.test.js @@ -0,0 +1,58 @@ +import constructQuery from '../../../src/utils/constructQuery'; + +let currentLimit; +let currentPage; +let currentCategory; +let currentAuthors; +let currentTags; +let currentSearchTerm; + +describe('TEST constructQuery() function', () => { + currentLimit = 10; + currentPage = 1; + currentCategory = 'fashion'; + currentAuthors = [1, 2]; + currentTags = [3, 4]; + currentSearchTerm = 'jsndlansdlja'; + it('should return a full, well-formed query', () => { + const result = constructQuery( + currentCategory, + currentSearchTerm, + currentLimit, + currentPage, + currentAuthors, + currentTags + ); + expect(result).toBe('fashion?search=jsndlansdlja&limit=10&page=1&tag=3 4&author=1 2'); + }); + it('should return a full, well-formed query', () => { + currentLimit = 10; + currentPage = 1; + currentCategory = 'fashion'; + currentAuthors = [1]; + currentTags = [3]; + currentSearchTerm = 'jsndlansdlja'; + const result = constructQuery( + currentCategory, + currentSearchTerm, + currentLimit, + currentPage, + currentAuthors, + currentTags + ); + expect(result).toBe('fashion?search=jsndlansdlja&limit=10&page=1&tag=3&author=1'); + }); + it('should return a full, well-formed query', () => { + currentLimit = 10; + currentPage = 1; + currentCategory = 'fashion'; + currentSearchTerm = ''; + const result = constructQuery( + currentCategory, + currentSearchTerm, + currentLimit, + currentPage, + ); + expect(result).toBe('fashion?limit=10&page=1'); + }); +}); diff --git a/tests/src/utils/detachOptions.test.js b/tests/src/utils/detachOptions.test.js new file mode 100644 index 0000000..d76df21 --- /dev/null +++ b/tests/src/utils/detachOptions.test.js @@ -0,0 +1,35 @@ +import detachOptions from '../../../src/utils/detachOptions'; +import mockAuthors from '../../../mockdata/authors'; +import mockCategories from '../../../mockdata/categories'; + +describe('Test detachOptions function', () => { + const { authors } = mockAuthors; + const { categories } = mockCategories; + it('should return an array of labels and integer values for authors', () => { + const result = detachOptions(authors, 'fullName'); + expect(result).toEqual( + [ + { value: 1, label: 'John Doe' }, + { value: 7, label: 'Solomon Kingsley' }, + { value: 3, label: 'Akanmu Christopher' }, + { value: 17, label: 'Augustine Ezinwa' } + ] + ); + }); + + it('should return an array of labels and values for categories', () => { + const result = detachOptions(categories, 'categoryName'); + expect(result).toEqual( + [ + { value: 'all', label: 'all' }, + { value: 'food', label: 'food' }, + { value: 'programming', label: 'programming' }, + { value: 'physics', label: 'physics' }, + { value: 'gaming', label: 'gaming' }, + { value: 'business', label: 'business' }, + { value: 'travel', label: 'travel' }, + { value: 'fitness', label: 'fitness' } + ] + ); + }); +}); diff --git a/tests/src/utils/paginationHelper.test.js b/tests/src/utils/paginationHelper.test.js index f5ef504..ee0f3eb 100644 --- a/tests/src/utils/paginationHelper.test.js +++ b/tests/src/utils/paginationHelper.test.js @@ -13,6 +13,6 @@ describe('TEST PAGINATION HELPER FUNCTION', () => { expect(resultThree).toEqual([1, '...', 16, 17, 18, 19]); expect(resultTFour).toEqual([1, '...', 17, 18, 19, 20]); expect(resultTFive).toEqual([1, 2, 3, 4, 5, 6]); - expect(resultSix).toEqual('error'); + expect(resultSix).toEqual([1]); }); });