diff --git a/CHANGELOG.md b/CHANGELOG.md index 823337a..1aa9955 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,27 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.6.0] - 2020-08-17 + +### 🐛 Bug Fixeroo + +### Added + +### Changed + +- Hide hidden jobs in search results - [#79](https://github.com/alexlee-dev/gh-jobs/issues/79) +- Single `ErrorResponse` type for error responses - [#88](https://github.com/alexlee-dev/gh-jobs/issues/88) + +### Removed + +- Template Configuration - [#93](https://github.com/alexlee-dev/gh-jobs/issues/93) + +### Fixed + +- `unique()` function not working as expected, resulting in search results containing more entires than jobs in DB - [#85](https://github.com/alexlee-dev/gh-jobs/issues/85) +- Now able to access `Details` page with a direct url - [#80](https://github.com/alexlee-dev/gh-jobs/issues/80) +- `ProfileAccountStats` container width spilling out on mobile - [#93](https://github.com/alexlee-dev/gh-jobs/issues/93) + ## [1.5.0] - 2020-08-15 ### ⏚ī¸ Button Redesign diff --git a/cypress/integration/applicationError.spec.js b/cypress/integration/applicationError.spec.js index ad19df7..c03f2ef 100644 --- a/cypress/integration/applicationError.spec.js +++ b/cypress/integration/applicationError.spec.js @@ -10,7 +10,7 @@ context("Application Error", () => { cy.fixture("jobs50").then((jobsJson) => { cy.server(); cy.route({ - method: "GET", + method: "POST", url: "/jobs", status: 200, response: jobsJson, @@ -18,7 +18,7 @@ context("Application Error", () => { }); cy.route({ method: "GET", - url: "/jobs/search?full_time=false&contract=false&description=react", + url: "/jobs/search?userId=&full_time=false&contract=false&description=react", status: 200, response: {}, delay: 1000, diff --git a/cypress/integration/details.spec.js b/cypress/integration/details.spec.js index a209950..ee43985 100644 --- a/cypress/integration/details.spec.js +++ b/cypress/integration/details.spec.js @@ -6,7 +6,7 @@ context("Details", () => { cy.fixture("jobDetails").then((jobDetails) => { cy.server(); cy.route({ - method: "GET", + method: "POST", url: "/jobs", status: 200, response: jobsJson, diff --git a/cypress/integration/directPageAccess.spec.js b/cypress/integration/directPageAccess.spec.js index 3437edb..7027ee8 100644 --- a/cypress/integration/directPageAccess.spec.js +++ b/cypress/integration/directPageAccess.spec.js @@ -1,15 +1,24 @@ /// -context("Details", () => { +context("Direct Page Access", () => { beforeEach(() => { cy.fixture("jobs50").then((jobsJson) => { - cy.server(); - cy.route({ - method: "GET", - url: "/jobs", - status: 200, - response: jobsJson, - delay: 1000, + cy.fixture("jobDetails").then((jobDetailsJson) => { + cy.server(); + cy.route({ + method: "POST", + url: "/jobs", + status: 200, + response: jobsJson, + delay: 1000, + }); + cy.route({ + method: "GET", + url: "/jobs/f1884b46-ecb4-473c-81f5-08d9bf2ab3bb", + status: 200, + response: jobDetailsJson, + delay: 1000, + }); }); }); }); @@ -28,8 +37,8 @@ context("Details", () => { cy.get("h1").should("have.text", "Create Account"); }); - it("Should be able to access '/jobs/:id' directly", () => { - cy.visit("http://localhost:3000/jobs/f1884b46-ecb4-473c-81f5-08d9bf2ab3bb"); + it("Should be able to access '/jobDetails/:id' directly", () => { + cy.visit("http://localhost:3000/jobDetails/f1884b46-ecb4-473c-81f5-08d9bf2ab3bb"); cy.wait(500); cy.get("h2").should("have.text", "Cloud DevOps Engineer"); diff --git a/cypress/integration/hiddenJobs.spec.js b/cypress/integration/hiddenJobs.spec.js index fc83232..4ff92f8 100644 --- a/cypress/integration/hiddenJobs.spec.js +++ b/cypress/integration/hiddenJobs.spec.js @@ -4,20 +4,29 @@ context("Hidden Jobs", () => { beforeEach(() => { cy.fixture("jobs50").then((jobsJson) => { cy.fixture("hiddenDetails").then((hiddenDetailsJson) => { - cy.server(); - cy.route({ - method: "GET", - url: "/jobs", - status: 200, - response: jobsJson, - delay: 1000, - }); - cy.route({ - method: "GET", - url: "/user/hiddenJobsDetails", - status: 200, - response: hiddenDetailsJson, - delay: 1000, + cy.fixture("jobDetails").then((jobDetailsJson) => { + cy.server(); + cy.route({ + method: "POST", + url: "/jobs", + status: 200, + response: jobsJson, + delay: 1000, + }); + cy.route({ + method: "GET", + url: "/user/hiddenJobsDetails", + status: 200, + response: hiddenDetailsJson, + delay: 1000, + }); + cy.route({ + method: "GET", + url: "/jobs/f1884b46-ecb4-473c-81f5-08d9bf2ab3bb", + status: 200, + response: jobDetailsJson, + delay: 1000, + }); }); }); }); @@ -223,6 +232,29 @@ context("Hidden Jobs", () => { cy.get("#show-job-72de09f2-5bc6-489f-be90-3d38e505e20a").click(); cy.get("#show-job-cc20d9f2-0102-4785-8253-66093d3ca5c0").click(); }); + + // ! Unable to do with current implementation + // * If you don't stub it, the real db may not contain that job listing anymore + // * If you do stub it, you can't conditionally send a smaller list of jobs each time it hits /user/hiddenJobDetails + it.skip("Should not display hidden jobs in currentJobs", () => { + // * Hide a job + cy.get("#hide-job-f1884b46-ecb4-473c-81f5-08d9bf2ab3bb").click(); + // * Log User out + cy.get("#nav-profile").click(); + cy.get("#settings").click(); + cy.get("#log-out").click(); + + cy.get("#f1884b46-ecb4-473c-81f5-08d9bf2ab3bb").should("exist"); + + // * Log In + cy.get("#nav-login").click(); + cy.get("#email").type("bobtest@email.com"); + cy.get("#password").type("Red123456!!!"); + cy.get("#log-in").click(); + cy.wait(500); + + cy.get("#f1884b46-ecb4-473c-81f5-08d9bf2ab3bb").should("not.exist"); + }); }); context("Hidden Jobs - No Results", () => { @@ -230,7 +262,7 @@ context("Hidden Jobs - No Results", () => { cy.fixture("jobs50").then((jobsJson) => { cy.server(); cy.route({ - method: "GET", + method: "POST", url: "/jobs", status: 200, response: jobsJson, @@ -254,7 +286,7 @@ context("Hidden Jobs - No Results", () => { cy.get("#nav-profile").click(); cy.get("#view-hidden-jobs").click(); - assert.equal(cy.state("requests").length, 3); + assert.equal(cy.state("requests").length, 4); }); it("Should display correct text", () => { diff --git a/cypress/integration/login.spec.js b/cypress/integration/login.spec.js index fae6a31..50901bb 100644 --- a/cypress/integration/login.spec.js +++ b/cypress/integration/login.spec.js @@ -6,7 +6,7 @@ context("Login - Success", () => { cy.fixture("login").then((loginJson) => { cy.server(); cy.route({ - method: "GET", + method: "POST", url: "/jobs", status: 200, response: jobsJson, @@ -48,7 +48,7 @@ context("Login - Error", () => { cy.fixture("jobs50").then((jobsJson) => { cy.server(); cy.route({ - method: "GET", + method: "POST", url: "/jobs", status: 200, response: jobsJson, diff --git a/cypress/integration/notification.spec.js b/cypress/integration/notification.spec.js index 9405d86..599b8f6 100644 --- a/cypress/integration/notification.spec.js +++ b/cypress/integration/notification.spec.js @@ -5,7 +5,7 @@ context("Notification", () => { cy.fixture("jobs50").then((jobsJson) => { cy.server(); cy.route({ - method: "GET", + method: "POST", url: "/jobs", status: 200, response: jobsJson, diff --git a/cypress/integration/optionsPanel.spec.js b/cypress/integration/optionsPanel.spec.js index 3d60ecb..2f253cb 100644 --- a/cypress/integration/optionsPanel.spec.js +++ b/cypress/integration/optionsPanel.spec.js @@ -8,7 +8,7 @@ context("Options Panel", () => { cy.fixture("jobsSearch3").then((jobsSearch3Json) => { cy.server(); cy.route({ - method: "GET", + method: "POST", url: "/jobs", status: 200, response: jobsJson, @@ -16,7 +16,7 @@ context("Options Panel", () => { cy.route({ method: "GET", url: - "/jobs/search?full_time=true&contract=false&description=developer", + "/jobs/search?userId=&full_time=true&contract=false&description=developer", status: 200, delay: 1000, response: jobsSearch2Json, @@ -24,7 +24,7 @@ context("Options Panel", () => { cy.route({ method: "GET", url: - "/jobs/search?full_time=false&contract=false&description=&location1=Los Angeles", + "/jobs/search?userId=&full_time=false&contract=false&description=&location1=Los Angeles", status: 200, delay: 1000, response: jobsSearch1Json, @@ -32,7 +32,7 @@ context("Options Panel", () => { cy.route({ method: "GET", url: - "/jobs/search?full_time=false&contract=false&description=&location1=Chicago", + "/jobs/search?userId=&full_time=false&contract=false&description=&location1=Chicago", status: 200, delay: 1000, response: jobsSearch1Json, @@ -40,7 +40,7 @@ context("Options Panel", () => { cy.route({ method: "GET", url: - "/jobs/search?full_time=false&contract=false&description=developer", + "/jobs/search?userId=&full_time=false&contract=false&description=developer", status: 200, delay: 1000, response: jobsSearch1Json, @@ -48,7 +48,7 @@ context("Options Panel", () => { cy.route({ method: "GET", url: - "/jobs/search?full_time=false&contract=false&description=&location1=Los Angeles", + "/jobs/search?userId=&full_time=false&contract=false&description=&location1=Los Angeles", status: 200, delay: 1000, response: jobsSearch1Json, @@ -56,7 +56,7 @@ context("Options Panel", () => { cy.route({ method: "GET", url: - "/jobs/search?full_time=false&contract=true&description=developer", + "/jobs/search?userId=&full_time=false&contract=true&description=developer", status: 200, delay: 1000, response: jobsSearch3Json, diff --git a/cypress/integration/pagination.spec.js b/cypress/integration/pagination.spec.js index 71f7856..fb1c734 100644 --- a/cypress/integration/pagination.spec.js +++ b/cypress/integration/pagination.spec.js @@ -5,7 +5,7 @@ context("Pagination", () => { cy.fixture("jobs50").then((jobsJson) => { cy.server(); cy.route({ - method: "GET", + method: "POST", url: "/jobs", status: 200, response: jobsJson, @@ -213,7 +213,7 @@ context("Pagination", () => { cy.server(); cy.route({ method: "GET", - url: "/jobs/search?full_time=false&contract=false&description=&location1=Chicago", + url: "/jobs/search?userId=&full_time=false&contract=false&description=&location1=Chicago", status: 200, response: jobsJson, }); @@ -258,7 +258,7 @@ context("Pagination - 1 Page", () => { cy.fixture("jobs5").then((jobsJson) => { cy.server(); cy.route({ - method: "GET", + method: "POST", url: "/jobs", status: 200, response: jobsJson, @@ -296,7 +296,7 @@ context("Pagination - 2 Pages", () => { cy.fixture("jobs10").then((jobsJson) => { cy.server(); cy.route({ - method: "GET", + method: "POST", url: "/jobs", status: 200, response: jobsJson, @@ -334,7 +334,7 @@ context("Pagination - 3 Pages", () => { cy.fixture("jobs15").then((jobsJson) => { cy.server(); cy.route({ - method: "GET", + method: "POST", url: "/jobs", status: 200, response: jobsJson, @@ -374,7 +374,7 @@ context("Pagination - 4 Pages", () => { cy.fixture("jobs20").then((jobsJson) => { cy.server(); cy.route({ - method: "GET", + method: "POST", url: "/jobs", status: 200, response: jobsJson, diff --git a/cypress/integration/profile.spec.js b/cypress/integration/profile.spec.js index 145fe49..ec6eec3 100644 --- a/cypress/integration/profile.spec.js +++ b/cypress/integration/profile.spec.js @@ -5,7 +5,7 @@ context("Profile", () => { cy.fixture("jobs50").then((jobsJson) => { cy.server(); cy.route({ - method: "GET", + method: "POST", url: "/jobs", status: 200, response: jobsJson, diff --git a/cypress/integration/savedJobs.spec.js b/cypress/integration/savedJobs.spec.js index a31070a..108147e 100644 --- a/cypress/integration/savedJobs.spec.js +++ b/cypress/integration/savedJobs.spec.js @@ -4,20 +4,29 @@ context("Saved Jobs", () => { beforeEach(() => { cy.fixture("jobs50").then((jobsJson) => { cy.fixture("savedDetails").then((savedDetailsJson) => { - cy.server(); - cy.route({ - method: "GET", - url: "/jobs", - status: 200, - response: jobsJson, - delay: 1000, - }); - cy.route({ - method: "GET", - url: "/user/savedJobsDetails", - status: 200, - response: savedDetailsJson, - delay: 1000, + cy.fixture("jobDetails").then((jobDetailsJson) => { + cy.server(); + cy.route({ + method: "POST", + url: "/jobs", + status: 200, + response: jobsJson, + delay: 1000, + }); + cy.route({ + method: "GET", + url: "/user/savedJobsDetails", + status: 200, + response: savedDetailsJson, + delay: 1000, + }); + cy.route({ + method: "GET", + url: "/jobs/f1884b46-ecb4-473c-81f5-08d9bf2ab3bb", + status: 200, + response: jobDetailsJson, + delay: 1000, + }); }); }); }); @@ -145,7 +154,7 @@ context("Saved Jobs - No Results", () => { cy.fixture("jobs50").then((jobsJson) => { cy.server(); cy.route({ - method: "GET", + method: "POST", url: "/jobs", status: 200, response: jobsJson, @@ -169,7 +178,7 @@ context("Saved Jobs - No Results", () => { cy.get("#nav-profile").click(); cy.get("#view-saved-jobs").click(); - assert.equal(cy.state("requests").length, 3); + assert.equal(cy.state("requests").length, 4); }); it("Should display correct text", () => { diff --git a/cypress/integration/search.spec.js b/cypress/integration/search.spec.js index 5ef1957..9ed1673 100644 --- a/cypress/integration/search.spec.js +++ b/cypress/integration/search.spec.js @@ -6,7 +6,7 @@ context("Search", () => { cy.fixture("jobsSearch1").then((searchJson) => { cy.server(); cy.route({ - method: "GET", + method: "POST", url: "/jobs", status: 200, response: jobsJson, @@ -14,7 +14,8 @@ context("Search", () => { }); cy.route({ method: "GET", - url: "/jobs/search?full_time=false&contract=false&description=developer", + url: + "/jobs/search?userId=&full_time=false&contract=false&description=developer", status: 200, response: searchJson, delay: 1000, @@ -50,13 +51,38 @@ context("Search", () => { cy.get("#search").type("{enter}"); cy.get('[data-cy="orbit-container"]').should("be.visible"); }); + + // ! Unable to do with current implementation + // * If you don't stub it, the real db may not contain that job listing anymore + // * If you do stub it, you can't conditionally send a smaller list of jobs each time it hits /user/hiddenJobDetails + it.skip("Should not display hidden jobs in currentJobs on search", () => { + // * Hide a job + cy.get("#hide-job-f1884b46-ecb4-473c-81f5-08d9bf2ab3bb").click(); + // * Log User out + cy.get("#nav-profile").click(); + cy.get("#settings").click(); + cy.get("#log-out").click(); + + cy.get("#f1884b46-ecb4-473c-81f5-08d9bf2ab3bb").should("exist"); + + // * Log In + cy.get("#nav-login").click(); + cy.get("#email").type("bobtest@email.com"); + cy.get("#password").type("Red123456!!!"); + cy.get("#log-in").click(); + cy.wait(500); + + cy.get("#f1884b46-ecb4-473c-81f5-08d9bf2ab3bb").should("not.exist"); + + // * Do search + }); }); context("Search - No Results", () => { beforeEach(() => { cy.server(); cy.route({ - method: "GET", + method: "POST", url: "/jobs", status: 200, response: [], @@ -64,7 +90,8 @@ context("Search - No Results", () => { }); cy.route({ method: "GET", - url: "/jobs/search?full_time=false&contract=false&description=developer", + url: + "/jobs/search?userId=&full_time=false&contract=false&description=developer", status: 200, response: [], delay: 1000, @@ -88,7 +115,7 @@ context("Search - Loading Indicator", () => { cy.fixture("jobs50").then((jobsJson) => { cy.server(); cy.route({ - method: "GET", + method: "POST", url: "/jobs", status: 200, response: jobsJson, diff --git a/cypress/integration/signup.spec.js b/cypress/integration/signup.spec.js index 88de989..0d7e931 100644 --- a/cypress/integration/signup.spec.js +++ b/cypress/integration/signup.spec.js @@ -6,7 +6,7 @@ context("Signup - Success", () => { cy.fixture("signup").then((signupJson) => { cy.server(); cy.route({ - method: "GET", + method: "POST", url: "/jobs", status: 200, response: jobsJson, @@ -53,7 +53,7 @@ context("Signup - Error", () => { cy.fixture("jobs50").then((jobsJson) => { cy.server(); cy.route({ - method: "GET", + method: "POST", url: "/jobs", status: 200, response: jobsJson, diff --git a/package-lock.json b/package-lock.json index 6c16623..70cfa7d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "gh-jobs", - "version": "1.5.0", + "version": "1.6.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 08d0bcd..0818518 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gh-jobs", - "version": "1.5.0", + "version": "1.6.0", "description": "A MERN application bootstrapped with create-mern-application.", "main": "build/index.js", "scripts": { diff --git a/src/client/App.tsx b/src/client/App.tsx index ad67819..12c4c81 100644 --- a/src/client/App.tsx +++ b/src/client/App.tsx @@ -56,7 +56,7 @@ const App: React.SFC = (props: AppProps) => { - +
diff --git a/src/client/components/JobCard/JobCard.tsx b/src/client/components/JobCard/JobCard.tsx index a2409f9..8c199f6 100644 --- a/src/client/components/JobCard/JobCard.tsx +++ b/src/client/components/JobCard/JobCard.tsx @@ -91,7 +91,7 @@ const JobCard: React.SFC = (props: JobCardProps) => { handleClearJobDetails()} - to={`/jobs/${job.id}`} + to={`/jobDetails/${job.id}`} > {job.title} diff --git a/src/client/components/Profile/Profile-styled.tsx b/src/client/components/Profile/Profile-styled.tsx index ba2d6f7..11bd5ee 100644 --- a/src/client/components/Profile/Profile-styled.tsx +++ b/src/client/components/Profile/Profile-styled.tsx @@ -84,7 +84,7 @@ const ProfileAccountStatsContainer = styled.div` @media only screen and (max-width: 820px) { margin-bottom: 50px; margin-top: 100px; - width: 100%; + width: auto; } `; diff --git a/src/client/redux/actionTypes.ts b/src/client/redux/actionTypes.ts index 7858d86..eba082d 100644 --- a/src/client/redux/actionTypes.ts +++ b/src/client/redux/actionTypes.ts @@ -24,6 +24,7 @@ export const SET_EDIT_NAME = "SET_EDIT_NAME"; export const SET_EMAIL = "SET_EMAIL"; export const SET_HIDDEN_JOBS = "SET_HIDDEN_JOBS"; export const SET_HIDDEN_JOBS_DETAILS = "SET_HIDDEN_JOBS_DETAILS"; +export const SET_ID = "SET_ID"; export const SET_IS_EDITING_PROFILE = "SET_IS_EDITING_PROFILE"; export const SET_IS_LOGGED_IN = "SET_IS_LOGGED_IN"; export const SET_NAME = "SET_NAME"; diff --git a/src/client/redux/actions/user.ts b/src/client/redux/actions/user.ts index 75ae5f8..979abdd 100644 --- a/src/client/redux/actions/user.ts +++ b/src/client/redux/actions/user.ts @@ -3,6 +3,7 @@ import { SET_EMAIL, SET_HIDDEN_JOBS, SET_HIDDEN_JOBS_DETAILS, + SET_ID, SET_IS_EDITING_PROFILE, SET_IS_LOGGED_IN, SET_NAME, @@ -38,6 +39,11 @@ export const setHiddenJobsDetails = (hiddenJobsDetails: Job[]): UserAction => ({ payload: { hiddenJobsDetails }, }); +export const setId = (id: string): UserAction => ({ + type: SET_ID, + payload: { id }, +}); + export const setIsEditingProfile = (isEditingProfile: boolean): UserAction => ({ type: SET_IS_EDITING_PROFILE, payload: { isEditingProfile }, diff --git a/src/client/redux/reducers/user.ts b/src/client/redux/reducers/user.ts index 5911427..92cd3cb 100644 --- a/src/client/redux/reducers/user.ts +++ b/src/client/redux/reducers/user.ts @@ -3,6 +3,7 @@ import { SET_EMAIL, SET_HIDDEN_JOBS, SET_HIDDEN_JOBS_DETAILS, + SET_ID, SET_IS_EDITING_PROFILE, SET_IS_LOGGED_IN, SET_NAME, @@ -23,6 +24,7 @@ export const initialState: UserState = { email: "", hiddenJobs: [], hiddenJobsDetails: [], + id: "", isEditingProfile: false, isLoggedIn: false, name: "", @@ -50,6 +52,7 @@ const reducer = (state = initialState, action: UserAction): UserState => { case SET_EMAIL: case SET_HIDDEN_JOBS: case SET_HIDDEN_JOBS_DETAILS: + case SET_ID: case SET_IS_EDITING_PROFILE: case SET_IS_LOGGED_IN: case SET_NAME: diff --git a/src/client/redux/thunks.ts b/src/client/redux/thunks.ts index ecd127f..9b4fe89 100644 --- a/src/client/redux/thunks.ts +++ b/src/client/redux/thunks.ts @@ -30,34 +30,28 @@ import { setSavedJobsTotalPages, setHiddenJobs, setHiddenJobsDetails, + setId, } from "./actions/user"; import { fetchServerData, isError } from "../util"; import { - AddHiddenJobErrorResponse, AddHiddenJobSuccessResponse, - AddSavedJobErrorResponse, AddSavedJobSuccessResponse, AppThunk, DeleteProfileResponse, EditProfileResponse, - GetHiddenJobsDetailsErrorResponse, + ErrorResponse, GetHiddenJobsDetailsSuccessResponse, - GetJobsErrorResponse, GetJobsSuccessResponse, - GetSavedJobsDetailsErrorResponse, GetSavedJobsDetailsSuccessResponse, Job, LocationOption, LoginResponse, - RemoveHiddenJobErrorResponse, RemoveHiddenJobSuccessResponse, - RemoveSavedJobErrorResponse, RemoveSavedJobSuccessResponse, ResetPasswordResponse, RootState, ServerResponseUser, - SignupErrorResponse, SignupSuccessResponse, } from "../types"; @@ -71,6 +65,7 @@ export const searchJobs = ( const state: RootState = getState(); const { contract, fullTime, locationSearch } = state.application; + const { id } = state.user; const locationsSearches = locationOptions.filter( (location: LocationOption) => location.value !== "" @@ -84,7 +79,7 @@ export const searchJobs = ( }); } - let url = `/jobs/search?full_time=${encodeURI( + let url = `/jobs/search?userId=${encodeURI(id)}&full_time=${encodeURI( fullTime.toString() )}&contract=${encodeURI(contract.toString())}&description=${encodeURI( search @@ -95,7 +90,7 @@ export const searchJobs = ( }); const data = (await fetchServerData(url, "GET")) as - | GetJobsErrorResponse + | ErrorResponse | GetJobsSuccessResponse; if (isError(data)) { @@ -137,9 +132,25 @@ export const logIn = (): AppThunk => async (dispatch, getState) => { return; } + // * Establish Job Data + const jobsResult = (await fetchServerData( + "/jobs", + "POST", + JSON.stringify({ userId: response._id }) + )) as ErrorResponse | GetJobsSuccessResponse; + + if (isError(jobsResult)) { + dispatch(displayNotification(jobsResult.error, "error")); + dispatch(setIsLoading(false)); + return; + } + dispatch(setIsLoggedIn(true)); + dispatch(setCurrentJobs(jobsResult)); + dispatch(setTotalPages(Math.ceil(jobsResult.length / 5))); dispatch(setEmail(response.email)); dispatch(setName(response.name)); + dispatch(setId(response._id)); dispatch(setSavedJobs(response.savedJobs)); dispatch(setHiddenJobs(response.hiddenJobs)); @@ -160,9 +171,7 @@ export const signup = (): AppThunk => async (dispatch, getState) => { } // TODO - Modify - const result: - | SignupErrorResponse - | SignupSuccessResponse = await fetchServerData( + const result: ErrorResponse | SignupSuccessResponse = await fetchServerData( "/user", "POST", JSON.stringify({ confirmPassword, email, name, password }) @@ -188,6 +197,8 @@ export const signup = (): AppThunk => async (dispatch, getState) => { export const initializeApplication = (): AppThunk => async (dispatch) => { try { dispatch(setIsLoading(true)); + + // * Reset State to Defaults dispatch(displayNotification("", "default")); dispatch(setError(null, null)); dispatch(setCurrentJobs([])); @@ -199,42 +210,41 @@ export const initializeApplication = (): AppThunk => async (dispatch) => { dispatch(setModalContent("")); dispatch(setModalTitle("")); - // * Establish Job Data - dispatch(setIsLoading(true)); - - const jobsResult = (await fetchServerData("/jobs", "GET")) as - | GetJobsErrorResponse - | GetJobsSuccessResponse; - - if (isError(jobsResult)) { - dispatch(displayNotification(jobsResult.error, "error")); - dispatch(setIsLoading(false)); - return; - } - - dispatch(setJobs(jobsResult)); - dispatch(setCurrentPage(1)); - dispatch(setTotalPages(Math.ceil(jobsResult.length / 5))); - dispatch(setCurrentJobs(jobsResult)); - // * Establish User Authentication const userResponse = await fetch("/user/me"); + let userId = ""; if (userResponse.status === 200) { + // * User is authenticated const user: ServerResponseUser = await userResponse.json(); - const nonHiddenJobs = jobsResult.filter( - (job: Job) => user.hiddenJobs.indexOf(job.id) < 0 - ); + userId = user._id; dispatch(setName(user.name)); dispatch(setEmail(user.email)); + dispatch(setId(userId)); dispatch(setSavedJobs(user.savedJobs)); dispatch(setHiddenJobs(user.hiddenJobs)); dispatch(setIsLoggedIn(true)); - dispatch(setCurrentJobs(nonHiddenJobs)); - dispatch(setTotalPages(Math.ceil(nonHiddenJobs.length / 5))); } + + // * Establish Job Data + const jobsResult = (await fetchServerData( + "/jobs", + "POST", + JSON.stringify({ userId }) + )) as ErrorResponse | GetJobsSuccessResponse; + + if (isError(jobsResult)) { + dispatch(displayNotification(jobsResult.error, "error")); + dispatch(setIsLoading(false)); + return; + } + + dispatch(setJobs(jobsResult)); + dispatch(setCurrentPage(1)); + dispatch(setTotalPages(Math.ceil(jobsResult.length / 5))); + dispatch(setCurrentJobs(jobsResult)); dispatch(setIsLoading(false)); } catch (error) { console.error(error); @@ -259,10 +269,26 @@ export const logOut = (): AppThunk => async (dispatch) => { return; } + // * Establish Job Data + const jobsResult = (await fetchServerData( + "/jobs", + "POST", + JSON.stringify({ userId: "" }) + )) as ErrorResponse | GetJobsSuccessResponse; + + if (isError(jobsResult)) { + dispatch(displayNotification(jobsResult.error, "error")); + dispatch(setIsLoading(false)); + return; + } + + dispatch(displayNotification("", "default")); + dispatch(setCurrentJobs(jobsResult)); + dispatch(setTotalPages(Math.ceil(jobsResult.length / 5))); dispatch(setConfirmPassword("")); dispatch(setEmail("")); - dispatch(displayNotification("", "default")); dispatch(setName("")); + dispatch(setId("")); dispatch(setPassword("")); dispatch(setSavedJobs([])); dispatch(setHiddenJobs([])); @@ -290,10 +316,26 @@ export const logOutAll = (): AppThunk => async (dispatch) => { return; } + // * Establish Job Data + const jobsResult = (await fetchServerData( + "/jobs", + "POST", + JSON.stringify({ userId: "" }) + )) as ErrorResponse | GetJobsSuccessResponse; + + if (isError(jobsResult)) { + dispatch(displayNotification(jobsResult.error, "error")); + dispatch(setIsLoading(false)); + return; + } + dispatch(setConfirmPassword("")); - dispatch(setEmail("")); dispatch(displayNotification("", "default")); + dispatch(setCurrentJobs(jobsResult)); + dispatch(setTotalPages(Math.ceil(jobsResult.length / 5))); + dispatch(setEmail("")); dispatch(setName("")); + dispatch(setId("")); dispatch(setPassword("")); dispatch(setSavedJobs([])); dispatch(setHiddenJobs([])); @@ -425,7 +467,7 @@ export const addHiddenJob = (id: string): AppThunk => async ( const { currentJobs } = state.application; // TODO - Modify const result: - | AddHiddenJobErrorResponse + | ErrorResponse | AddHiddenJobSuccessResponse = await fetchServerData( "/user/hiddenJobs", "PATCH", @@ -459,7 +501,7 @@ export const addSavedJob = (id: string): AppThunk => async (dispatch) => { try { // TODO - Modify const result: - | AddSavedJobErrorResponse + | ErrorResponse | AddSavedJobSuccessResponse = await fetchServerData( "/user/savedJobs", "PATCH", @@ -491,7 +533,7 @@ export const removeHiddenJob = (id: string): AppThunk => async (dispatch) => { try { // TODO - Modify const result: - | RemoveHiddenJobErrorResponse + | ErrorResponse | RemoveHiddenJobSuccessResponse = await fetchServerData( "/user/hiddenJobs", "PATCH", @@ -521,7 +563,7 @@ export const removeSavedJob = (id: string): AppThunk => async (dispatch) => { try { // TODO - Modify const result: - | RemoveSavedJobErrorResponse + | ErrorResponse | RemoveSavedJobSuccessResponse = await fetchServerData( "/user/savedJobs", "PATCH", @@ -592,7 +634,7 @@ export const getSavedJobsDetails = (): AppThunk => async (dispatch) => { try { const result: - | GetSavedJobsDetailsErrorResponse + | ErrorResponse | GetSavedJobsDetailsSuccessResponse = await fetchServerData( `/user/savedJobsDetails`, "GET" @@ -619,7 +661,7 @@ export const getHiddenJobsDetails = (): AppThunk => async (dispatch) => { try { const result: - | GetHiddenJobsDetailsErrorResponse + | ErrorResponse | GetHiddenJobsDetailsSuccessResponse = await fetchServerData( `/user/hiddenJobsDetails`, "GET" diff --git a/src/client/types.ts b/src/client/types.ts index dcd3753..f967c28 100644 --- a/src/client/types.ts +++ b/src/client/types.ts @@ -1,10 +1,6 @@ import { Action } from "redux"; import { ThunkAction } from "redux-thunk"; -export interface AddHiddenJobErrorResponse { - error: string; -} - export interface AddHiddenJobSuccessResponse { createdAt: string; email: string; @@ -16,10 +12,6 @@ export interface AddHiddenJobSuccessResponse { _id: string; } -export interface AddSavedJobErrorResponse { - error: string; -} - export interface AddSavedJobSuccessResponse { createdAt: string; email: string; @@ -69,32 +61,20 @@ export type ButtonStyle = "primary" | "secondary" | "danger"; export type ButtonType = "button" | "reset" | "submit"; -export type DeleteProfileResponse = ServerResponseError & ServerResponseUser; +export type DeleteProfileResponse = ErrorResponse & ServerResponseUser; -export type EditProfileResponse = ServerResponseError & ServerResponseUser; +export type EditProfileResponse = ErrorResponse & ServerResponseUser; -export interface GetJobDetailsErrorResponse { +export interface ErrorResponse { error: string; } export type GetJobDetailsSuccessResponse = Job; -export interface GetJobsErrorResponse { - error: string; -} - export type GetJobsSuccessResponse = Job[]; -export interface GetHiddenJobsDetailsErrorResponse { - error: string; -} - export type GetHiddenJobsDetailsSuccessResponse = Job[]; -export interface GetSavedJobsDetailsErrorResponse { - error: string; -} - export type GetSavedJobsDetailsSuccessResponse = Job[]; export type InputAutoComplete = @@ -188,7 +168,7 @@ export interface LocationOption { value: string; } -export type LoginResponse = ServerResponseError & ServerResponseUser; +export type LoginResponse = ErrorResponse & ServerResponseUser; export interface ModalAction { type: string; @@ -212,10 +192,6 @@ export type NotificationType = export type PaginationNavigationType = "left" | "right"; -export interface RemoveHiddenJobErrorResponse { - error: string; -} - export interface RemoveHiddenJobSuccessResponse { createdAt: string; email: string; @@ -227,10 +203,6 @@ export interface RemoveHiddenJobSuccessResponse { _id: string; } -export interface RemoveSavedJobErrorResponse { - error: string; -} - export interface RemoveSavedJobSuccessResponse { createdAt: string; email: string; @@ -244,7 +216,7 @@ export interface RemoveSavedJobSuccessResponse { export type RequestMethod = "DELETE" | "GET" | "PATCH" | "POST"; -export type ResetPasswordResponse = ServerResponseError & ServerResponseUser; +export type ResetPasswordResponse = ErrorResponse & ServerResponseUser; export type RootState = { application: ApplicationState; @@ -254,10 +226,6 @@ export type RootState = { export type SearchType = "description" | "location"; -export interface ServerResponseError { - error: string; -} - export interface ServerResponseUser { createdAt: string; email: string; @@ -269,10 +237,6 @@ export interface ServerResponseUser { _id: string; } -export interface SignupErrorResponse { - error: string; -} - export interface SignupSuccessResponse { createdAt: string; email: string; @@ -295,6 +259,7 @@ export interface UserState { email: string; hiddenJobs: string[]; hiddenJobsDetails: Job[]; + id: string; isEditingProfile: boolean; isLoggedIn: false; name: string; diff --git a/src/client/util.ts b/src/client/util.ts index 9db4e22..cf88855 100644 --- a/src/client/util.ts +++ b/src/client/util.ts @@ -1,14 +1,11 @@ import { createBrowserHistory } from "history"; import { + ErrorResponse, RequestMethod, - GetJobDetailsErrorResponse, GetJobDetailsSuccessResponse, - GetJobsErrorResponse, GetJobsSuccessResponse, - AddSavedJobErrorResponse, AddSavedJobSuccessResponse, - SignupErrorResponse, SignupSuccessResponse, } from "./types"; @@ -30,32 +27,6 @@ export const fetchServerData = async ( return data; }; -// TODO - Remove -// eslint-disable-next-line -export const groupBy = (arr: any[], key: any): any => - arr.reduce( - (acc, item) => ((acc[item[key]] = [...(acc[item[key]] || []), item]), acc), - {} - ); - -// TODO - Remove -// eslint-disable-next-line -export const unique = (arr: any[]): any[] => [...new Set(arr)]; - -// TODO - Remove -export const validURL = (str: string): boolean => { - const pattern = new RegExp( - "^(https?:\\/\\/)?" + // protocol - "((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|" + // domain name - "((\\d{1,3}\\.){3}\\d{1,3}))" + // OR ip (v4) address - "(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*" + // port and path - "(\\?[;&a-z\\d%_.~+=-]*)?" + // query string - "(\\#[-a-z\\d_]*)?$", - "i" - ); // fragment locator - return !!pattern.test(str); -}; - /** * Loads the state of the application from localStorage if present. * @returns {object} @@ -89,16 +60,13 @@ export const saveState = (state: any): void => { export const isError = ( result: - | GetJobsErrorResponse - | GetJobsSuccessResponse - | GetJobDetailsErrorResponse - | GetJobDetailsSuccessResponse - | AddSavedJobErrorResponse | AddSavedJobSuccessResponse - | SignupErrorResponse + | ErrorResponse + | GetJobDetailsSuccessResponse + | GetJobsSuccessResponse | SignupSuccessResponse -): result is GetJobsErrorResponse => { - return (result as GetJobsErrorResponse).error !== undefined; +): result is ErrorResponse => { + return (result as ErrorResponse).error !== undefined; }; export const history = createBrowserHistory(); diff --git a/src/server/app.ts b/src/server/app.ts index 4062a38..8c10af4 100644 --- a/src/server/app.ts +++ b/src/server/app.ts @@ -85,10 +85,16 @@ class App { // ? Use this.app.all? console.log( - chalk.blueBright.inverse({ - host: req.headers.host, - referrer: req.headers.referer, - }) + chalk.blueBright.inverse( + JSON.stringify( + { + host: req.headers.host, + referrer: req.headers.referer, + }, + null, + 2 + ) + ) ); res.sendFile(path.join(__dirname, "../dist/index.html")); }); diff --git a/src/server/controllers/job.ts b/src/server/controllers/job.ts index 0ef0e37..073a76d 100644 --- a/src/server/controllers/job.ts +++ b/src/server/controllers/job.ts @@ -7,16 +7,16 @@ import path from "path"; import JobModel from "../models/Job"; -import { getAllJobsFromAPI, isError, unique } from "../util"; +import { unique, rehydrateJobsDB } from "../util"; import { - GetJobsErrorResponse, + ErrorResponse, GetJobsSuccessResponse, Job, - GetJobDetailsErrorResponse, GetJobDetailsSuccessResponse, - GitHubJob, + JobDocument, } from "../types"; +import User from "../models/User"; /** * Job Controller. @@ -29,40 +29,39 @@ class JobController { } public initializeRoutes(): void { - this.router.get( + this.router.post( "/jobs", async ( req: Request, res: Response - ): Promise> => { + ): Promise> => { try { - const currentJobs = await JobModel.find({}); + // * Get User Information + const userId: string = req.body.userId; + let user = null; + let hiddenJobs: string[] = []; + if (userId !== "") { + user = await User.findById(userId); + hiddenJobs = user.hiddenJobs; + } + + // * Get Job Information + let dbJobs: JobDocument[] = await JobModel.find({}); // * No Jobs exist in DB - if (currentJobs.length === 0) { - const result = await getAllJobsFromAPI(); + if (dbJobs.length === 0) { + const rehydrationResult = await rehydrateJobsDB(); - if (isError(result)) { - return res.status(500).send(result); + // TODO - Check for this in a different way + if (rehydrationResult !== true) { + return res.status(500).send(rehydrationResult); } - await Promise.all( - result.map(async (job: GitHubJob) => { - const newJobObject: Job = { - ...job, - listingDate: job.created_at, - }; - const newJob = new JobModel(newJobObject); - await newJob.save(); - return; - }) - ); - - const dbJobs = await JobModel.find({}); - return res.send(dbJobs); + // * Set dbJobs to new jobs + dbJobs = await JobModel.find({}); } else { - // * Jobs exist in DB - const { createdAt } = currentJobs[0]; + // * Jobs exist in DB, but we need to ensure they are not stale jobs (from yesterday) + const { createdAt } = dbJobs[0]; const isWithinToday = isWithinInterval(new Date(createdAt), { start: startOfToday(), @@ -71,35 +70,24 @@ class JobController { if (!isWithinToday) { // * Jobs are stale. Get new jobs. - const result = await getAllJobsFromAPI(); + const rehydration2Result = await rehydrateJobsDB(); - if (isError(result)) { - return res.status(500).send(result); + // TODO - Check for this in a different way + if (rehydration2Result !== true) { + return res.status(500).send(rehydration2Result); } - // * Drop the current database of Jobs - await JobModel.collection.drop(); - - // * Create new Job entries - await Promise.all( - result.map(async (job: GitHubJob) => { - const newJobObject: Job = { - ...job, - listingDate: job.created_at, - }; - const newJob = new JobModel(newJobObject); - await newJob.save(); - return; - }) - ); - - const dbJobs = await JobModel.find({}); - return res.send(dbJobs); - } else { - // * Jobs are fine, send that. - return res.send(currentJobs); + // * Set dbJobs to new jobs + dbJobs = await JobModel.find({}); } } + + // * Ensure that the user's hiddenJobs do not show in results + const filteredDBJobs: JobDocument[] = dbJobs.filter( + (jobDocument: JobDocument) => hiddenJobs.indexOf(jobDocument.id) < 0 + ); + + return res.send(filteredDBJobs); } catch (error) { if (process.env.NODE_ENV !== "test") { console.error(error); @@ -115,7 +103,7 @@ class JobController { async ( req: Request, res: Response - ): Promise> => { + ): Promise> => { try { const { contract, @@ -126,6 +114,7 @@ class JobController { location3, location4, location5, + userId, } = req.query; const isLocationSearch = @@ -170,8 +159,19 @@ class JobController { ); const uniqueResults: Job[] = unique(jobs); + let finalResults = uniqueResults; + + // * Filter by non-hidden jobs + if (userId) { + // * Get User Information + const user = await User.findById(userId); + const hiddenJobs: string[] = user.hiddenJobs; + finalResults = uniqueResults.filter( + (job: Job) => hiddenJobs.indexOf(job.id) < 0 + ); + } - return res.send(uniqueResults); + return res.send(finalResults); } // * Make Searches @@ -203,8 +203,19 @@ class JobController { ]; const uniqueResults: Job[] = unique(searchResults); + let finalResults = uniqueResults; + + // * Filter by non-hidden jobs + if (userId) { + // * Get User Information + const user = await User.findById(userId); + const hiddenJobs: string[] = user.hiddenJobs; + finalResults = uniqueResults.filter( + (job: Job) => hiddenJobs.indexOf(job.id) < 0 + ); + } - return res.send(uniqueResults); + return res.send(finalResults); } catch (error) { if (process.env.NODE_ENV !== "test") { console.error(error); @@ -220,7 +231,7 @@ class JobController { req: Request, res: Response ): Promise | void> => { if (!req.headers.referer) { return res.sendFile(path.join(__dirname, "../../dist/index.html")); diff --git a/src/server/controllers/user.ts b/src/server/controllers/user.ts index e4e006d..2218e66 100644 --- a/src/server/controllers/user.ts +++ b/src/server/controllers/user.ts @@ -12,15 +12,13 @@ import { AuthenticatedRequest, EditHiddenJobsMethod, EditSavedJobsMethod, - GetHiddenJobsDetailsErrorResponse, + ErrorResponse, GetHiddenJobsDetailsSuccessResponse, - GetSavedJobsDetailsErrorResponse, GetSavedJobsDetailsSuccessResponse, - PatchSavedJobErrorResponse, + Job, PatchSavedJobSuccessResponse, Token, UserDocument, - Job, } from "../types"; /** @@ -216,9 +214,7 @@ class UserController { async ( req: AuthenticatedRequest, res: Response - ): Promise< - Response - > => { + ): Promise> => { try { const method: EditSavedJobsMethod = req.body.method; const id: string = req.body.id; @@ -259,9 +255,7 @@ class UserController { async ( req: AuthenticatedRequest, res: Response - ): Promise< - Response - > => { + ): Promise> => { try { const method: EditHiddenJobsMethod = req.body.method; const id: string = req.body.id; @@ -303,9 +297,7 @@ class UserController { req: AuthenticatedRequest, res: Response ): Promise< - Response< - GetSavedJobsDetailsErrorResponse | GetSavedJobsDetailsSuccessResponse - > + Response > => { try { const { savedJobs } = req.user; @@ -347,10 +339,7 @@ class UserController { req: AuthenticatedRequest, res: Response ): Promise< - Response< - | GetHiddenJobsDetailsErrorResponse - | GetHiddenJobsDetailsSuccessResponse - > + Response > => { try { const { hiddenJobs } = req.user; diff --git a/src/server/types.ts b/src/server/types.ts index 1c2a006..8f30d67 100644 --- a/src/server/types.ts +++ b/src/server/types.ts @@ -15,28 +15,16 @@ export type EditHiddenJobsMethod = "ADD" | "REMOVE"; export type EditSavedJobsMethod = "ADD" | "REMOVE"; -export interface GetJobDetailsErrorResponse { +export interface ErrorResponse { error: string; } export type GetJobDetailsSuccessResponse = Job; -export interface GetJobsErrorResponse { - error: string; -} - export type GetJobsSuccessResponse = GitHubJob[]; -export interface GetHiddenJobsDetailsErrorResponse { - error: string; -} - export type GetHiddenJobsDetailsSuccessResponse = Job[]; -export interface GetSavedJobsDetailsErrorResponse { - error: string; -} - export type GetSavedJobsDetailsSuccessResponse = Job[]; export interface GitHubJob { @@ -87,10 +75,6 @@ export interface JobDocument extends Document { export type JobType = "Contract" | "Full Time"; -export interface PatchSavedJobErrorResponse { - error: string; -} - export interface PatchSavedJobSuccessResponse { createdAt: string; email: string; diff --git a/src/server/util.ts b/src/server/util.ts index d4f7942..069c57a 100644 --- a/src/server/util.ts +++ b/src/server/util.ts @@ -1,10 +1,8 @@ import nfetch from "node-fetch"; -import { - GetJobsErrorResponse, - GetJobsSuccessResponse, - GitHubJob, -} from "./types"; +import JobModel from "./models/Job"; + +import { ErrorResponse, GetJobsSuccessResponse, GitHubJob, Job } from "./types"; /** * Check if MongoDB is running locally. Stops application from continuing if false. @@ -50,7 +48,7 @@ export const createSearchURL = ( }; export const getAllJobsFromAPI = async (): Promise< - GetJobsErrorResponse | GetJobsSuccessResponse + ErrorResponse | GetJobsSuccessResponse > => { const jobs: GitHubJob[] = []; let jobsInBatch = null; @@ -80,10 +78,42 @@ export const getAllJobsFromAPI = async (): Promise< }; export const isError = ( - result: GetJobsErrorResponse | GetJobsSuccessResponse -): result is GetJobsErrorResponse => { - return (result as GetJobsErrorResponse).error !== undefined; + result: ErrorResponse | GetJobsSuccessResponse +): result is ErrorResponse => { + return (result as ErrorResponse).error !== undefined; }; // eslint-disable-next-line -export const unique = (arr: any[]): any[] => [...new Set(arr)]; +export const unique = (arr: any[]): any[] => + [...new Set(arr.map((item) => JSON.stringify(item)))].map((item) => + JSON.parse(item) + ); + +export const rehydrateJobsDB = async (): Promise => { + try { + const result = await getAllJobsFromAPI(); + + if (isError(result)) { + return result; + } + // * Drop the current database of Jobs + await JobModel.collection.drop(); + + // * Create new Job entries + await Promise.all( + result.map(async (job: GitHubJob) => { + const newJobObject: Job = { + ...job, + listingDate: job.created_at, + }; + const newJob = new JobModel(newJobObject); + await newJob.save(); + return; + }) + ); + + return true; + } catch (error) { + console.error(error); + } +}; diff --git a/template-tsconfig.json b/template-tsconfig.json deleted file mode 100644 index 1554740..0000000 --- a/template-tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "compilerOptions": { - "jsx": "preserve", - "target": "ESNext", - "module": "ESNext", - "moduleResolution": "node", - "outDir": "./dist", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true - }, - "include": ["./src/**/*", "./index.d.ts"] -}