/gm)[0];
-
- setApplyLink(href);
- }
-
- setData(job);
- }, []);
-
- const jobIsSaved =
- savedJobs && data
- ? savedJobs.findIndex((savedJob: Job) => savedJob.id === data.id) >= 0
- : false;
-
- return (
- <>
-
-
-
-
west
-
Back to search
-
-
-
-
-
- {notificationMessage && (
-
- )}
- {data && (
- <>
-
-
-
{data.title}
-
- {data.type === "Full Time" && (
-
- Full Time
-
- )}
- {isLoggedIn && (
-
- )}
-
-
-
-
-
access_time
-
- {formatDistanceToNow(new Date(data.created_at), {
- addSuffix: true,
- })}
-
-
-
-
-
-
- {data.company_logo ? (
-
- ) : (
-
- )}
-
-
-
- {data.company_url ? (
-
- {data.company}
-
- ) : (
-
{data.company}
- )}
-
-
-
public
-
{data.location}
-
-
-
-
-
- >
- )}
-
-
-
- >
- );
-};
-
-const mapStateToProps = (state: RootState) => ({
- isLoggedIn: state.user.isLoggedIn,
- jobs: state.application.jobs,
- notificationMessage: state.application.notificationMessage,
- notificationType: state.application.notificationType,
- savedJobs: state.user.savedJobs,
-});
-
-const mapDispatchToProps = (dispatch) => ({
- handleAddSavedJob: (job: Job) => dispatch(addSavedJob(job)),
- handleRemoveSavedJob: (job: Job) => dispatch(removeSavedJob(job)),
-});
-
-export default connect(mapStateToProps, mapDispatchToProps)(Details);
diff --git a/src/client/pages/Details/Details-styled.tsx b/src/client/pages/Details/Details-styled.tsx
new file mode 100644
index 0000000..7f46df4
--- /dev/null
+++ b/src/client/pages/Details/Details-styled.tsx
@@ -0,0 +1,277 @@
+import styled from "styled-components";
+
+const DetailsContainer = styled.div`
+ display: flex;
+ flex-direction: row;
+
+ @media only screen and (max-width: 600px) {
+ flex-direction: column;
+ }
+`;
+
+const DetailsSideContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+ padding-right: 50px;
+ width: 25%;
+
+ @media only screen and (max-width: 600px) {
+ width: 100%;
+ }
+
+ a {
+ align-items: center;
+ color: #1e86ff;
+ display: flex;
+ font-family: Poppins, sans-serif;
+ font-size: 14px;
+ font-weight: 500;
+ line-height: 21px;
+ margin-right: 15px;
+ justify-content: flex-start;
+ text-decoration: none;
+
+ i {
+ font-size: 16px;
+ margin-right: 5px;
+ }
+
+ :hover {
+ i {
+ text-decoration: none;
+ }
+
+ span {
+ text-decoration: underline;
+ }
+ }
+ }
+`;
+
+const DetailsHowToContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+ margin-top: 36px;
+ overflow-wrap: break-word;
+`;
+
+const DetailsHowToLabel = styled.span`
+ color: #b9bdcf;
+ font-family: Poppins, sans-serif;
+ font-size: 14px;
+ font-style: normal;
+ font-weight: bold;
+ line-height: 21px;
+ text-transform: uppercase;
+`;
+
+const DetailsMainContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+ width: 75%;
+
+ @media only screen and (max-width: 600px) {
+ width: 100%;
+ }
+`;
+
+const DetailsMainTitleContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+`;
+
+interface DetailsMainInnerTitleContainerProps {
+ jobIsSaved: boolean;
+}
+
+const DetailsMainInnerTitleContainer = styled.div<
+ DetailsMainInnerTitleContainerProps
+>`
+ align-items: flex-start;
+ display: flex;
+ flex-direction: column;
+
+ h2 {
+ color: #334680;
+ font-family: Roboto;
+ font-style: normal;
+ font-weight: bold;
+ font-size: 24px;
+ line-height: 28px;
+ margin-bottom: 0;
+ margin-top: 0;
+
+ @media only screen and (max-width: 600px) {
+ margin-top: 36px;
+ }
+ }
+
+ div {
+ display: flex;
+ flex-direction: row;
+ margin-top: 15px;
+
+ p {
+ border: 1px solid #334680;
+ border-radius: 4px;
+ color: #334680;
+ font-size: 12px;
+ font-weight: bold;
+ line-height: 14px;
+ margin-bottom: 0;
+ margin-top: 0;
+ padding: 6px 8px;
+ text-align: center;
+ width: 53px;
+
+ @media only screen and (max-width: 600px) {
+ margin-left: 0;
+ margin-top: 4px;
+ }
+ }
+
+ button {
+ background: transparent;
+ border: none;
+ color: ${(props) => (props.jobIsSaved ? "#1e86ff" : "#b9bdcf")};
+ margin: 0;
+ margin-left: 15px;
+ padding: 0;
+
+ :hover {
+ color: #1e86ff;
+ cursor: pointer;
+ }
+ }
+ }
+`;
+
+const DetailsCreatedContainer = styled.div`
+ align-items: center;
+ display: flex;
+ align-items: start;
+ margin-left: 0;
+ margin-top: 10px;
+
+ i {
+ color: #b9bdcf;
+ font-size: 15px;
+ margin-right: 7.5px;s
+ }
+
+ p {
+ color: #b9bdcf;
+ font-size: 12px;
+ font-weight: 500;
+ line-height: 14px;
+ margin-bottom: 0;
+ margin-top: 0;
+ }
+`;
+
+const DetailsCompanyContainer = styled.div`
+ display: flex;
+ flex-direction: row;
+ margin-top: 32px;
+`;
+
+const DetailsLogoContainer = styled.div`
+ background-color: #f2f2f2;
+ border-radius: 4px;
+ height: 42px;
+ margin-right: 12px;
+ width: 42px;
+
+ div {
+ align-items: center;
+ display: flex;
+ height: 100%;
+ justify-content: center;
+ text-align: center;
+ width: 100%;
+
+ p {
+ color: #bdbdbd;
+ font-size: 8px;
+ font-weight: 500;
+ line-height: 14px;
+ }
+ }
+
+ img {
+ height: 42px;
+ object-fit: contain;
+ width: 42px;
+ }
+`;
+
+const DetailsCompanyRightContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+
+ a {
+ color: #334680;
+ font-family: Roboto;
+ font-size: 18px;
+ font-style: normal;
+ font-weight: bold;
+ line-height: 21px;
+ margin: 0;
+ text-decoration: none;
+ }
+
+ div {
+ align-items: flex-start;
+ display: flex;
+ margin-top: 10px;
+
+ i {
+ color: #b9bdcf;
+ font-size: 15px;
+ margin-right: 5px;
+ }
+
+ p {
+ color: #b9bdcf;
+ font-size: 12px;
+ font-weight: 500;
+ line-height: 14px;
+ margin-top: 0;
+ }
+ }
+
+ p {
+ color: #334680;
+ font-family: Roboto;
+ font-size: 18px;
+ font-style: normal;
+ font-weight: bold;
+ line-height: 21px;
+ margin: 0;
+ text-decoration: none;
+ }
+`;
+
+const DetailsContainerDescription = styled.div`
+ color: #334680;
+ font-family: Roboto;
+ font-style: normal;
+ font-weight: normal;
+ font-size: 16px;
+ line-height: 24px;
+`;
+
+export {
+ DetailsContainer,
+ DetailsSideContainer,
+ DetailsHowToContainer,
+ DetailsHowToLabel,
+ DetailsMainContainer,
+ DetailsMainTitleContainer,
+ DetailsMainInnerTitleContainer,
+ DetailsCreatedContainer,
+ DetailsCompanyContainer,
+ DetailsLogoContainer,
+ DetailsCompanyRightContainer,
+ DetailsContainerDescription,
+};
diff --git a/src/client/pages/Details/Details.tsx b/src/client/pages/Details/Details.tsx
new file mode 100644
index 0000000..ed0fe01
--- /dev/null
+++ b/src/client/pages/Details/Details.tsx
@@ -0,0 +1,209 @@
+import * as React from "react";
+import { connect } from "react-redux";
+import formatDistanceToNow from "date-fns/formatDistanceToNow";
+import { useParams, Link } from "react-router-dom";
+
+import Copyright from "../../components/Copyright";
+
+import {
+ DetailsContainer,
+ DetailsSideContainer,
+ DetailsHowToContainer,
+ DetailsHowToLabel,
+ DetailsMainContainer,
+ DetailsMainTitleContainer,
+ DetailsMainInnerTitleContainer,
+ DetailsCreatedContainer,
+ DetailsCompanyContainer,
+ DetailsLogoContainer,
+ DetailsCompanyRightContainer,
+ DetailsContainerDescription,
+} from "./Details-styled";
+
+import { addSavedJob, getJobDetails, removeSavedJob } from "../../redux/thunks";
+
+import { Job, RootState } from "../../types";
+
+interface DetailsProps {
+ handleAddSavedJob: (id: string) => void;
+ handleGetJobDetails: (id: string) => void;
+ handleRemoveSavedJob: (id: string) => void;
+ jobDetails: Job;
+ isLoggedIn: boolean;
+ savedJobs: string[];
+}
+
+const Details: React.SFC = (props: DetailsProps) => {
+ const { id } = useParams();
+ const {
+ handleAddSavedJob,
+ handleGetJobDetails,
+ handleRemoveSavedJob,
+ jobDetails,
+ isLoggedIn,
+ savedJobs,
+ } = props;
+
+ const [applyLink, setApplyLink] = React.useState("");
+
+ React.useEffect((): void => {
+ window.scrollTo(0, 0);
+ handleGetJobDetails(id);
+ }, []);
+
+ const jobIsSaved =
+ savedJobs && jobDetails
+ ? savedJobs.findIndex(
+ (savedJobID: string) => savedJobID === jobDetails.id
+ ) >= 0
+ : false;
+
+ React.useEffect((): void => {
+ if (jobDetails) {
+ const isPlainLink = jobDetails.how_to_apply.slice(0, 5) === "/gm)[0];
+
+ setApplyLink(href);
+ }
+ }
+ }, [jobDetails]);
+
+ return (
+ <>
+
+
+
+ west
+ Back to search
+
+
+
+ How to Apply
+
+ {jobDetails &&
+ (applyLink ? (
+
+ link
+ Apply
+
+ ) : (
+
+ ))}
+
+
+
+
+ {jobDetails && (
+ <>
+
+
+ {jobDetails.title}
+
+ {jobDetails.type === "Full Time" && (
+
Full Time
+ )}
+ {isLoggedIn && (
+
+ )}
+
+
+
+
+ access_time
+
+ {formatDistanceToNow(new Date(jobDetails.created_at), {
+ addSuffix: true,
+ })}
+
+
+
+
+
+
+ {jobDetails.company_logo ? (
+
+ ) : (
+
+ )}
+
+
+
+ {jobDetails.company_url ? (
+
+ {jobDetails.company}
+
+ ) : (
+ {jobDetails.company}
+ )}
+
+
+
public
+
{jobDetails.location}
+
+
+
+
+
+
+
+ >
+ )}
+
+
+
+ >
+ );
+};
+
+const mapStateToProps = (state: RootState) => ({
+ isLoggedIn: state.user.isLoggedIn,
+ jobDetails: state.application.jobDetails,
+ savedJobs: state.user.savedJobs,
+});
+
+const mapDispatchToProps = (dispatch) => ({
+ handleAddSavedJob: (id: string) => dispatch(addSavedJob(id)),
+ handleGetJobDetails: (id: string) => dispatch(getJobDetails(id)),
+ handleRemoveSavedJob: (id: string) => dispatch(removeSavedJob(id)),
+});
+
+export default connect(mapStateToProps, mapDispatchToProps)(Details);
diff --git a/src/client/pages/Details/index.ts b/src/client/pages/Details/index.ts
new file mode 100644
index 0000000..ee80bfc
--- /dev/null
+++ b/src/client/pages/Details/index.ts
@@ -0,0 +1 @@
+export { default } from "./Details";
diff --git a/src/client/pages/Login/Login-styled.tsx b/src/client/pages/Login/Login-styled.tsx
new file mode 100644
index 0000000..6672059
--- /dev/null
+++ b/src/client/pages/Login/Login-styled.tsx
@@ -0,0 +1,87 @@
+import styled from "styled-components";
+
+const LoginContainer = styled.div`
+ align-items: center;
+ display: flex;
+ flex-direction: column;
+
+ form {
+ max-width: 444px;
+ width: 50%;
+
+ @media only screen and (max-width: 600px) {
+ width: 100%;
+ }
+ }
+`;
+
+const LoginTitleContainer = styled.div`
+ align-items: center;
+ display: flex;
+ flex-direction: column;
+
+ h1 {
+ color: #282538;
+ font-family: Poppins;
+ font-style: normal;
+ font-weight: 200;
+ font-size: 24px;
+ line-height: 36px;
+ margin: 0;
+ }
+
+ span {
+ align-items: center;
+ background: #1e86ff;
+ border-radius: 50%;
+ display: flex;
+ height: 40px;
+ justify-content: center;
+ margin: 8px;
+ width: 40px;
+
+ i {
+ color: #ffffff;
+ }
+ }
+`;
+
+const LoginActionsContainer = styled.div`
+ align-items: center;
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+
+ a {
+ align-items: center;
+ color: #1e86ff;
+ display: flex;
+ font-family: Poppins, sans-serif;
+ font-size: 14px;
+ font-weight: 500;
+ line-height: 21px;
+ justify-content: flex-start;
+ text-decoration: none;
+
+ :hover {
+ span {
+ text-decoration: underline;
+ }
+
+ i {
+ text-decoration: none;
+ }
+ }
+
+ i {
+ font-size: 18px;
+ margin-right: 10px;
+ }
+ }
+
+ @media only screen and (max-width: 600px) {
+ flex-direction: column;
+ }
+`;
+
+export { LoginContainer, LoginTitleContainer, LoginActionsContainer };
diff --git a/src/client/pages/Login.tsx b/src/client/pages/Login/Login.tsx
similarity index 67%
rename from src/client/pages/Login.tsx
rename to src/client/pages/Login/Login.tsx
index cbbfa4a..2866030 100644
--- a/src/client/pages/Login.tsx
+++ b/src/client/pages/Login/Login.tsx
@@ -2,19 +2,23 @@ import * as React from "react";
import { connect } from "react-redux";
import { Link, Redirect } from "react-router-dom";
-import Button from "../components/Button";
-import Copyright from "../components/Copyright";
-import Notification from "../components/Notification";
-import Input from "../components/Input";
+import Button from "../../components/Button";
+import Copyright from "../../components/Copyright";
+import Input from "../../components/Input";
-import { setEmail, setPassword } from "../redux/actions/user";
-import { logIn } from "../redux/thunks";
+import {
+ LoginContainer,
+ LoginTitleContainer,
+ LoginActionsContainer,
+} from "./Login-styled";
-import { RootState } from "../types";
+import { setEmail, setPassword } from "../../redux/actions/user";
+import { logIn } from "../../redux/thunks";
+
+import { RootState } from "../../types";
export interface LoginProps {
email: string;
- notificationMessage: string;
handleEmailChange: (email: string) => void;
handleLogIn: () => void;
handlePasswordChange: (password: string) => void;
@@ -25,7 +29,6 @@ export interface LoginProps {
const Login: React.SFC = (props: LoginProps) => {
const {
email,
- notificationMessage,
handleEmailChange,
handleLogIn,
handlePasswordChange,
@@ -37,26 +40,23 @@ const Login: React.SFC = (props: LoginProps) => {
return ;
} else {
return (
-
+
);
}
};
const mapStateToProps = (state: RootState) => ({
email: state.user.email,
- notificationMessage: state.application.notificationMessage,
isLoggedIn: state.user.isLoggedIn,
password: state.user.password,
});
diff --git a/src/client/pages/Login/index.ts b/src/client/pages/Login/index.ts
new file mode 100644
index 0000000..cd3a8ca
--- /dev/null
+++ b/src/client/pages/Login/index.ts
@@ -0,0 +1 @@
+export { default } from "./Login";
diff --git a/src/client/pages/Profile.tsx b/src/client/pages/Profile/Profile.tsx
similarity index 62%
rename from src/client/pages/Profile.tsx
rename to src/client/pages/Profile/Profile.tsx
index 5488a8d..b5152e7 100644
--- a/src/client/pages/Profile.tsx
+++ b/src/client/pages/Profile/Profile.tsx
@@ -2,14 +2,21 @@ import * as React from "react";
import { connect } from "react-redux";
import { Redirect } from "react-router-dom";
-import Notification from "../components/Notification";
-import ProfileDelete from "../components/ProfileDelete";
-import ProfileDisplay from "../components/ProfileDisplay";
-import ProfileEdit from "../components/ProfileEdit";
-import ProfileReset from "../components/ProfileReset";
-import ProfileSavedJobs from "../components/ProfileSavedJobs";
+import {
+ ProfileDelete,
+ ProfileDisplay,
+ ProfileEdit,
+ ProfileReset,
+ ProfileSavedJobs,
+} from "../../components/Profile";
-import { NotificationType, RootState } from "../types";
+import {
+ ProfilePage,
+ ProfileForm,
+ ProfileTitleContainer,
+} from "../../components/Profile/Profile-styled";
+
+import { RootState } from "../../types";
export interface ProfileProps {
isDeletingProfile: boolean;
@@ -17,8 +24,6 @@ export interface ProfileProps {
isLoggedIn: boolean;
isResettingPassword: boolean;
isViewingSavedJobs: boolean;
- notificationMessage: string;
- notificationType: NotificationType;
}
const Profile: React.SFC = (props: ProfileProps) => {
@@ -28,8 +33,6 @@ const Profile: React.SFC = (props: ProfileProps) => {
isLoggedIn,
isResettingPassword,
isViewingSavedJobs,
- notificationMessage,
- notificationType,
} = props;
let heading = "Profile";
@@ -48,21 +51,17 @@ const Profile: React.SFC = (props: ProfileProps) => {
return ;
} else {
return (
-
+
+
);
}
};
@@ -88,8 +87,6 @@ const mapStateToProps = (state: RootState) => ({
isLoggedIn: state.user.isLoggedIn,
isResettingPassword: state.user.isResettingPassword,
isViewingSavedJobs: state.user.isViewingSavedJobs,
- notificationMessage: state.application.notificationMessage,
- notificationType: state.application.notificationType,
});
export default connect(mapStateToProps)(Profile);
diff --git a/src/client/pages/Profile/index.ts b/src/client/pages/Profile/index.ts
new file mode 100644
index 0000000..2623c86
--- /dev/null
+++ b/src/client/pages/Profile/index.ts
@@ -0,0 +1 @@
+export { default } from "./Profile";
diff --git a/src/client/pages/Search/Search-styled.tsx b/src/client/pages/Search/Search-styled.tsx
new file mode 100644
index 0000000..a6246f0
--- /dev/null
+++ b/src/client/pages/Search/Search-styled.tsx
@@ -0,0 +1,31 @@
+import styled from "styled-components";
+
+const SearchContainer = styled.div`
+ display: flex;
+ margin-top: 42px;
+ @media only screen and (max-width: 800px) {
+ flex-direction: column;
+ }
+
+ @media only screen and (max-width: 600px) {
+ flex-direction: column;
+ }
+`;
+
+const SearchJobsContainer = styled.div`
+ width: 75%;
+
+ @media only screen and (max-width: 800px) {
+ width: 100%;
+ }
+
+ @media only screen and (max-width: 600px) {
+ width: 100%;
+ }
+`;
+
+const SearchNoResults = styled.p`
+ text-align: center;
+`;
+
+export { SearchContainer, SearchJobsContainer, SearchNoResults };
diff --git a/src/client/pages/Search.tsx b/src/client/pages/Search/Search.tsx
similarity index 53%
rename from src/client/pages/Search.tsx
rename to src/client/pages/Search/Search.tsx
index 57f03e8..05be43f 100644
--- a/src/client/pages/Search.tsx
+++ b/src/client/pages/Search/Search.tsx
@@ -1,31 +1,28 @@
import * as React from "react";
import { connect } from "react-redux";
-import Copyright from "../components/Copyright";
-import JobCard from "../components/JobCard";
-import Notification from "../components/Notification";
-import OptionsPanel from "../components/OptionsPanel";
-import Pagination from "../components/Pagination";
-import SearchInput from "../components/SearchInput";
+import Copyright from "../../components/Copyright";
+import JobCard from "../../components/JobCard";
+import OptionsPanel from "../../components/OptionsPanel";
+import Pagination from "../../components/Pagination";
+import SearchInput from "../../components/SearchInput";
-import { Job, LocationOption, NotificationType, RootState } from "../types";
+import {
+ SearchContainer,
+ SearchJobsContainer,
+ SearchNoResults,
+} from "./Search-styled";
+
+import { Job, LocationOption, RootState } from "../../types";
export interface SearchProps {
currentJobs: Job[];
currentPage: number;
- notificationMessage: string;
- notificationType: NotificationType;
totalPages: number;
}
const Search: React.SFC = (props: SearchProps) => {
- const {
- currentJobs,
- currentPage,
- notificationMessage,
- notificationType,
- totalPages,
- } = props;
+ const { currentJobs, currentPage, totalPages } = props;
const jobsOnPage = currentJobs.slice(currentPage * 5 - 5, currentPage * 5);
@@ -35,10 +32,10 @@ const Search: React.SFC = (props: SearchProps) => {
const [location4, setLocation4] = React.useState("");
const locationOptions: LocationOption[] = [
- { name: "location1", setter: setLocation1, value: location1 },
- { name: "location2", setter: setLocation2, value: location2 },
- { name: "location3", setter: setLocation3, value: location3 },
- { name: "location4", setter: setLocation4, value: location4 },
+ { name: "Chicago", setter: setLocation1, value: location1 },
+ { name: "Los Angeles", setter: setLocation2, value: location2 },
+ { name: "New York City", setter: setLocation3, value: location3 },
+ { name: "San Francisco", setter: setLocation4, value: location4 },
];
const handleCheckBox = (e) => {
@@ -56,27 +53,24 @@ const Search: React.SFC = (props: SearchProps) => {
return (
<>
-
-
-
- {notificationMessage && (
-
- )}
+
+
+
{jobsOnPage &&
jobsOnPage.map((job: Job) => )}
{jobsOnPage.length > 0 && (
)}
{jobsOnPage.length === 0 && (
-
+
No results. Please modify your search and try again.
-
+
)}
-
-
+
+
>
);
@@ -85,8 +79,6 @@ const Search: React.SFC = (props: SearchProps) => {
const mapStateToProps = (state: RootState) => ({
currentJobs: state.application.currentJobs,
currentPage: state.application.currentPage,
- notificationMessage: state.application.notificationMessage,
- notificationType: state.application.notificationType,
totalPages: state.application.totalPages,
});
diff --git a/src/client/pages/Search/index.ts b/src/client/pages/Search/index.ts
new file mode 100644
index 0000000..4ff9149
--- /dev/null
+++ b/src/client/pages/Search/index.ts
@@ -0,0 +1 @@
+export { default } from "./Search";
diff --git a/src/client/pages/Signup/Signup-styled.tsx b/src/client/pages/Signup/Signup-styled.tsx
new file mode 100644
index 0000000..a09def8
--- /dev/null
+++ b/src/client/pages/Signup/Signup-styled.tsx
@@ -0,0 +1,60 @@
+import styled from "styled-components";
+
+const SignupContainer = styled.div`
+ align-items: center;
+ display: flex;
+ flex-direction: column;
+
+ form {
+ max-width: 444px;
+ width: 50%;
+
+ @media only screen and (max-width: 600px) {
+ width: 100%;
+ }
+ }
+`;
+
+const SignupTitleContainer = styled.div`
+ align-items: center;
+ display: flex;
+ flex-direction: column;
+
+ h1 {
+ color: #282538;
+ font-family: Poppins;
+ font-style: normal;
+ font-weight: 200;
+ font-size: 24px;
+ line-height: 36px;
+ margin: 0;
+ }
+
+ span {
+ align-items: center;
+ background: #1e86ff;
+ border-radius: 50%;
+ display: flex;
+ height: 40px;
+ justify-content: center;
+ margin: 8px;
+ width: 40px;
+
+ i {
+ color: #ffffff;
+ }
+ }
+`;
+
+const SignupActionsContainer = styled.div`
+ align-items: center;
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+
+ @media only screen and (max-width: 600px) {
+ flex-direction: column;
+ }
+`;
+
+export { SignupContainer, SignupTitleContainer, SignupActionsContainer };
diff --git a/src/client/pages/Signup.tsx b/src/client/pages/Signup/Signup.tsx
similarity index 80%
rename from src/client/pages/Signup.tsx
rename to src/client/pages/Signup/Signup.tsx
index d989acd..1e6806d 100644
--- a/src/client/pages/Signup.tsx
+++ b/src/client/pages/Signup/Signup.tsx
@@ -2,25 +2,29 @@ import * as React from "react";
import { connect } from "react-redux";
import { Redirect } from "react-router-dom";
-import Button from "../components/Button";
-import Copyright from "../components/Copyright";
-import Notification from "../components/Notification";
-import Input from "../components/Input";
+import Button from "../../components/Button";
+import Copyright from "../../components/Copyright";
+import Input from "../../components/Input";
+
+import {
+ SignupContainer,
+ SignupTitleContainer,
+ SignupActionsContainer,
+} from "./Signup-styled";
import {
setConfirmPassword,
setEmail,
setName,
setPassword,
-} from "../redux/actions/user";
-import { signup } from "../redux/thunks";
+} from "../../redux/actions/user";
+import { signup } from "../../redux/thunks";
-import { RootState } from "../types";
+import { RootState } from "../../types";
export interface SignupProps {
confirmPassword: string;
email: string;
- notificationMessage: string;
handleConfirmPasswordChange: (confirmPassword: string) => void;
handleEmailChange: (email: string) => void;
handleNameChange: (name: string) => void;
@@ -35,7 +39,6 @@ const Signup: React.SFC = (props: SignupProps) => {
const {
confirmPassword,
email,
- notificationMessage,
handleConfirmPasswordChange,
handleEmailChange,
handleNameChange,
@@ -50,26 +53,23 @@ const Signup: React.SFC = (props: SignupProps) => {
return ;
} else {
return (
-
+
);
}
};
@@ -129,7 +132,6 @@ const Signup: React.SFC = (props: SignupProps) => {
const mapStateToProps = (state: RootState) => ({
confirmPassword: state.user.confirmPassword,
email: state.user.email,
- notificationMessage: state.application.notificationMessage,
isLoggedIn: state.user.isLoggedIn,
name: state.user.name,
password: state.user.password,
diff --git a/src/client/pages/Signup/index.ts b/src/client/pages/Signup/index.ts
new file mode 100644
index 0000000..6480e37
--- /dev/null
+++ b/src/client/pages/Signup/index.ts
@@ -0,0 +1 @@
+export { default } from "./Signup";
diff --git a/src/client/redux/actionTypes.ts b/src/client/redux/actionTypes.ts
index b4e535c..231d640 100644
--- a/src/client/redux/actionTypes.ts
+++ b/src/client/redux/actionTypes.ts
@@ -1,10 +1,11 @@
// * Application
+export const DISPLAY_NOTIFICATION = "DISPLAY_NOTIFICATION";
export const SET_CURRENT_JOBS = "SET_CURRENT_JOBS";
export const SET_CURRENT_PAGE = "SET_CURRENT_PAGE";
export const SET_FULL_TIME = "SET_FULL_TIME";
export const SET_IS_LOADING = "SET_IS_LOADING";
+export const SET_JOB_DETAILS = "SET_JOB_DETAILS";
export const SET_JOBS = "SET_JOBS";
-export const SET_JOBS_FETCHED_AT = "SET_JOBS_FETCHED_AT";
export const SET_LOCATION_SEARCH = "SET_LOCATION_SEARCH";
export const SET_SEARCH_VALUE = "SET_SEARCH_VALUE";
export const SET_TOTAL_PAGES = "SET_TOTAL_PAGES";
@@ -20,12 +21,11 @@ export const SET_IS_LOGGED_IN = "SET_IS_LOGGED_IN";
export const SET_IS_RESETTING_PASSWORD = "SET_IS_RESETTING_PASSWORD";
export const SET_IS_VIEWING_SAVED_JOBS = "SET_IS_VIEWING_SAVED_JOBS";
export const SET_NAME = "SET_NAME";
-export const SET_NOTIFICATION_MESSAGE = "SET_NOTIFICATION_MESSAGE";
-export const SET_NOTIFICATION_TYPE = "SET_NOTIFICATION_TYPE";
export const SET_PASSWORD = "SET_PASSWORD";
export const SET_RESET_CONFIRM_NEW_PASSWORD = "SET_RESET_CONFIRM_NEW_PASSWORD";
export const SET_RESET_CURRENT_PASSWORD = "SET_RESET_CURRENT_PASSWORD";
export const SET_RESET_NEW_PASSWORD = "SET_RESET_NEW_PASSWORD";
export const SET_SAVED_JOBS = "SET_SAVED_JOBS";
export const SET_SAVED_JOBS_CURRENT_PAGE = "SET_SAVED_JOBS_CURRENT_PAGE";
+export const SET_SAVED_JOBS_DETAILS = "SET_SAVED_JOBS_DETAILS";
export const SET_SAVED_JOBS_TOTAL_PAGES = "SET_SAVED_JOBS_TOTAL_PAGES";
diff --git a/src/client/redux/actions/application.ts b/src/client/redux/actions/application.ts
index dd309dc..af48ad3 100644
--- a/src/client/redux/actions/application.ts
+++ b/src/client/redux/actions/application.ts
@@ -1,19 +1,26 @@
import {
- SET_JOBS,
- SET_JOBS_FETCHED_AT,
- SET_FULL_TIME,
- SET_IS_LOADING,
+ DISPLAY_NOTIFICATION,
SET_CURRENT_JOBS,
SET_CURRENT_PAGE,
+ SET_FULL_TIME,
+ SET_IS_LOADING,
+ SET_JOB_DETAILS,
+ SET_JOBS,
SET_LOCATION_SEARCH,
- SET_NOTIFICATION_MESSAGE,
- SET_NOTIFICATION_TYPE,
SET_SEARCH_VALUE,
SET_TOTAL_PAGES,
} from "../actionTypes";
import { ApplicationAction, Job, NotificationType } from "../../types";
+export const displayNotification = (
+ notificationMessage: string,
+ notificationType: NotificationType
+): ApplicationAction => ({
+ type: DISPLAY_NOTIFICATION,
+ payload: { notificationMessage, notificationType },
+});
+
export const setCurrentJobs = (currentJobs: Job[]): ApplicationAction => ({
type: SET_CURRENT_JOBS,
payload: { currentJobs },
@@ -34,16 +41,16 @@ export const setIsLoading = (isLoading: boolean): ApplicationAction => ({
payload: { isLoading },
});
+export const setJobDetails = (jobDetails: Job): ApplicationAction => ({
+ type: SET_JOB_DETAILS,
+ payload: { jobDetails },
+});
+
export const setJobs = (jobs: Job[]): ApplicationAction => ({
type: SET_JOBS,
payload: { jobs },
});
-export const setJobsFetchedAt = (jobsFetchedAt: string): ApplicationAction => ({
- type: SET_JOBS_FETCHED_AT,
- payload: { jobsFetchedAt },
-});
-
export const setLocationSearch = (
locationSearch: string
): ApplicationAction => ({
@@ -51,20 +58,6 @@ export const setLocationSearch = (
payload: { locationSearch },
});
-export const setNotificationMessage = (
- notificationMessage: string
-): ApplicationAction => ({
- type: SET_NOTIFICATION_MESSAGE,
- payload: { notificationMessage },
-});
-
-export const setNotificationType = (
- notificationType: NotificationType
-): ApplicationAction => ({
- type: SET_NOTIFICATION_TYPE,
- payload: { notificationType },
-});
-
export const setSearchValue = (searchValue: string): ApplicationAction => ({
type: SET_SEARCH_VALUE,
payload: { searchValue },
diff --git a/src/client/redux/actions/user.ts b/src/client/redux/actions/user.ts
index 705bc21..3a9cefe 100644
--- a/src/client/redux/actions/user.ts
+++ b/src/client/redux/actions/user.ts
@@ -15,10 +15,11 @@ import {
SET_RESET_NEW_PASSWORD,
SET_SAVED_JOBS,
SET_SAVED_JOBS_CURRENT_PAGE,
+ SET_SAVED_JOBS_DETAILS,
SET_SAVED_JOBS_TOTAL_PAGES,
} from "../actionTypes";
-import { Job, UserAction } from "../../types";
+import { UserAction, Job } from "../../types";
export const setConfirmPassword = (confirmPassword: string): UserAction => ({
type: SET_CONFIRM_PASSWORD,
@@ -100,7 +101,7 @@ export const setResetNewPassword = (resetNewPassword: string): UserAction => ({
payload: { resetNewPassword },
});
-export const setSavedJobs = (savedJobs: Job[]): UserAction => ({
+export const setSavedJobs = (savedJobs: string[]): UserAction => ({
type: SET_SAVED_JOBS,
payload: { savedJobs },
});
@@ -112,6 +113,11 @@ export const setSavedJobsCurrentPage = (
payload: { savedJobsCurrentPage },
});
+export const setSavedJobsDetails = (savedJobsDetails: Job[]): UserAction => ({
+ type: SET_SAVED_JOBS_DETAILS,
+ payload: { savedJobsDetails },
+});
+
export const setSavedJobsTotalPages = (
savedJobsTotalPages: number
): UserAction => ({
diff --git a/src/client/redux/reducers/application.ts b/src/client/redux/reducers/application.ts
index c9b050b..7c41594 100644
--- a/src/client/redux/reducers/application.ts
+++ b/src/client/redux/reducers/application.ts
@@ -1,13 +1,14 @@
+import { toast } from "react-toastify";
+
import {
+ DISPLAY_NOTIFICATION,
SET_CURRENT_JOBS,
SET_CURRENT_PAGE,
SET_FULL_TIME,
SET_IS_LOADING,
+ SET_JOB_DETAILS,
SET_JOBS,
- SET_JOBS_FETCHED_AT,
SET_LOCATION_SEARCH,
- SET_NOTIFICATION_MESSAGE,
- SET_NOTIFICATION_TYPE,
SET_SEARCH_VALUE,
SET_TOTAL_PAGES,
} from "../actionTypes";
@@ -19,11 +20,11 @@ export const initialState: ApplicationState = {
currentPage: 1,
fullTime: false,
isLoading: true,
+ jobDetails: null,
jobs: [],
- jobsFetchedAt: null,
locationSearch: "",
notificationMessage: "",
- notificationType: "info",
+ notificationType: "default",
searchValue: "",
totalPages: 1,
};
@@ -41,15 +42,37 @@ const reducer = (
}
switch (action.type) {
+ case DISPLAY_NOTIFICATION: {
+ const { notificationMessage, notificationType } = action.payload;
+ if (notificationMessage) {
+ let autoClose: boolean | number = 5000;
+ if (notificationType === "error" || notificationType === "warning") {
+ autoClose = false;
+ }
+ toast(notificationMessage, {
+ autoClose,
+ toastId: "notification",
+ type: notificationType,
+ });
+ } else {
+ // * If displayNotification() is called with `notificationMessage` === "",
+ // * Clear all notifications
+ toast.dismiss();
+ }
+
+ return {
+ ...state,
+ notificationMessage,
+ notificationType,
+ };
+ }
case SET_CURRENT_JOBS:
case SET_CURRENT_PAGE:
case SET_FULL_TIME:
case SET_IS_LOADING:
+ case SET_JOB_DETAILS:
case SET_JOBS:
- case SET_JOBS_FETCHED_AT:
case SET_LOCATION_SEARCH:
- case SET_NOTIFICATION_MESSAGE:
- case SET_NOTIFICATION_TYPE:
case SET_SEARCH_VALUE:
case SET_TOTAL_PAGES: {
return { ...state, [key]: value };
diff --git a/src/client/redux/reducers/user.ts b/src/client/redux/reducers/user.ts
index ff527b6..243b649 100644
--- a/src/client/redux/reducers/user.ts
+++ b/src/client/redux/reducers/user.ts
@@ -15,6 +15,7 @@ import {
SET_RESET_NEW_PASSWORD,
SET_SAVED_JOBS,
SET_SAVED_JOBS_CURRENT_PAGE,
+ SET_SAVED_JOBS_DETAILS,
SET_SAVED_JOBS_TOTAL_PAGES,
} from "../actionTypes";
@@ -37,6 +38,7 @@ export const initialState: UserState = {
resetNewPassword: "",
savedJobs: [],
savedJobsCurrentPage: 1,
+ savedJobsDetails: [],
savedJobsTotalPages: 1,
};
@@ -66,6 +68,7 @@ const reducer = (state = initialState, action: UserAction): UserState => {
case SET_RESET_NEW_PASSWORD:
case SET_SAVED_JOBS:
case SET_SAVED_JOBS_CURRENT_PAGE:
+ case SET_SAVED_JOBS_DETAILS:
case SET_SAVED_JOBS_TOTAL_PAGES: {
return { ...state, [key]: value };
}
diff --git a/src/client/redux/thunks.ts b/src/client/redux/thunks.ts
index ce24d10..48927a1 100644
--- a/src/client/redux/thunks.ts
+++ b/src/client/redux/thunks.ts
@@ -1,17 +1,12 @@
-import endOfToday from "date-fns/endOfToday";
-import isWithinInterval from "date-fns/isWithinInterval";
-import startOfToday from "date-fns/startOfToday";
-
import {
+ displayNotification,
setCurrentJobs,
setCurrentPage,
setIsLoading,
setJobs,
- setJobsFetchedAt,
setSearchValue,
setTotalPages,
- setNotificationMessage,
- setNotificationType,
+ setJobDetails,
} from "./actions/application";
import {
setConfirmPassword,
@@ -30,34 +25,49 @@ import {
setResetNewPassword,
setSavedJobs,
setSavedJobsCurrentPage,
+ setSavedJobsDetails,
setSavedJobsTotalPages,
} from "./actions/user";
-import { fetchServerData, unique } from "../util";
+import { fetchServerData, isError } from "../util";
import {
- AddSavedJobResponse,
AppThunk,
DeleteProfileResponse,
EditProfileResponse,
+ GetJobsErrorResponse,
+ GetJobsSuccessResponse,
+ GetSavedJobsDetailsErrorResponse,
+ GetSavedJobsDetailsSuccessResponse,
Job,
LocationOption,
LoginResponse,
- RemoveSavedJobResponse,
ResetPasswordResponse,
RootState,
ServerResponseUser,
- SignupResponse,
+ AddSavedJobErrorResponse,
+ AddSavedJobSuccessResponse,
+ SignupErrorResponse,
+ SignupSuccessResponse,
+ RemoveSavedJobErrorResponse,
+ RemoveSavedJobSuccessResponse,
} from "../types";
export const getJobs = (): AppThunk => async (dispatch) => {
try {
- const jobs: Job[] = await fetchServerData("/jobs", "GET");
+ const result = (await fetchServerData("/jobs", "GET")) as
+ | GetJobsErrorResponse
+ | GetJobsSuccessResponse;
+
+ if (isError(result)) {
+ dispatch(displayNotification(result.error, "error"));
+ dispatch(setIsLoading(false));
+ return;
+ }
- dispatch(setJobs(jobs));
- dispatch(setJobsFetchedAt(new Date().toString()));
+ dispatch(setJobs(result));
dispatch(setCurrentPage(1));
- dispatch(setTotalPages(Math.ceil(jobs.length / 5)));
- dispatch(setCurrentJobs(jobs));
+ dispatch(setTotalPages(Math.ceil(result.length / 5)));
+ dispatch(setCurrentJobs(result));
dispatch(setIsLoading(false));
} catch (error) {
console.error(error);
@@ -69,12 +79,12 @@ export const searchJobs = (
locationOptions: LocationOption[]
): AppThunk => async (dispatch, getState) => {
dispatch(setIsLoading(true));
+ dispatch(displayNotification("", "default"));
dispatch(setSearchValue(search));
+
const state: RootState = getState();
const { fullTime, locationSearch } = state.application;
- const jobs = [];
-
const locationsSearches = locationOptions.filter(
(location: LocationOption) => location.value !== ""
);
@@ -87,38 +97,30 @@ export const searchJobs = (
});
}
- // * Since location options have to be a thing for the challenge
- // * Make as many requests as locations (since you can only have 1 location per request)
- // * And push all the results into one array
- await Promise.all(
- locationsSearches.map(async (location: LocationOption) => {
- const url = `/jobs/search?full_time=${encodeURI(
- fullTime.toString()
- )}&description=${encodeURI(search)}&location=${encodeURI(
- location.value
- )}`;
- const data = await fetchServerData(url, "GET");
- jobs.push(...data);
- })
- );
+ let url = `/jobs/search?full_time=${encodeURI(
+ fullTime.toString()
+ )}&description=${encodeURI(search)}`;
- if (locationsSearches.length === 0) {
- const url = `/jobs/search?full_time=${encodeURI(
- fullTime.toString()
- )}&description=${encodeURI(search)}`;
- const data = await fetchServerData(url, "GET");
- jobs.push(...data);
- }
+ locationsSearches.forEach((locationSearch: LocationOption, i: number) => {
+ url = url + `&location${i + 1}=${encodeURI(locationSearch.value)}`;
+ });
- const uniqueJobs = unique(jobs);
+ const data = (await fetchServerData(url, "GET")) as
+ | GetJobsErrorResponse
+ | GetJobsSuccessResponse;
- const finalJobs = uniqueJobs.filter((job: Job) =>
- fullTime ? job.type === "Full Time" : job
- );
+ if (isError(data)) {
+ dispatch(displayNotification(data.error, "error"));
+ dispatch(setIsLoading(false));
+ return;
+ }
- dispatch(setCurrentJobs(finalJobs));
+ dispatch(setCurrentJobs(data));
dispatch(setCurrentPage(1));
- dispatch(setTotalPages(Math.ceil(finalJobs.length / 5)));
+ dispatch(setTotalPages(Math.ceil(data.length / 5)));
+ dispatch(
+ displayNotification(`Search returned ${data.length} results.`, "success")
+ );
dispatch(setIsLoading(false));
};
@@ -128,11 +130,12 @@ export const pagination = (pageNumber: number): AppThunk => (dispatch) => {
export const logIn = (): AppThunk => async (dispatch, getState) => {
dispatch(setIsLoading(true));
- dispatch(setNotificationMessage(""));
+ dispatch(displayNotification("", "default"));
const { user } = getState();
const { email, password } = user;
+ // TODO - Modify
const response: LoginResponse = await fetchServerData(
"/user/login",
"POST",
@@ -140,8 +143,7 @@ export const logIn = (): AppThunk => async (dispatch, getState) => {
);
if (response.error) {
- dispatch(setNotificationType("error"));
- dispatch(setNotificationMessage(response.error));
+ dispatch(displayNotification(response.error, "error"));
dispatch(setIsLoading(false));
return;
}
@@ -156,63 +158,48 @@ export const logIn = (): AppThunk => async (dispatch, getState) => {
export const signup = (): AppThunk => async (dispatch, getState) => {
dispatch(setIsLoading(true));
- dispatch(setNotificationMessage(""));
+ dispatch(displayNotification("", "default"));
const { user } = getState();
const { confirmPassword, email, name, password } = user;
if (confirmPassword !== password) {
- dispatch(setNotificationType("error"));
- dispatch(setNotificationMessage("Passwords do not match."));
+ dispatch(displayNotification("Passwords do not match.", "error"));
dispatch(setIsLoading(false));
return;
}
- const response: SignupResponse = await fetchServerData(
+ // TODO - Modify
+ const result:
+ | SignupErrorResponse
+ | SignupSuccessResponse = await fetchServerData(
"/user",
"POST",
JSON.stringify({ confirmPassword, email, name, password })
);
- if (response.error) {
- dispatch(setNotificationType("error"));
- dispatch(setNotificationMessage(response.error));
+ if (isError(result)) {
+ dispatch(displayNotification(result.error, "error"));
dispatch(setIsLoading(false));
return;
}
dispatch(setIsLoggedIn(true));
- dispatch(setEmail(response.email));
- dispatch(setName(response.name));
+ dispatch(setEmail(result.email));
+ dispatch(setName(result.name));
dispatch(setPassword(""));
dispatch(setConfirmPassword(""));
- dispatch(setSavedJobs(response.savedJobs));
+ dispatch(setSavedJobs(result.savedJobs));
dispatch(setIsLoading(false));
};
-export const initializeApplication = (): AppThunk => async (
- dispatch,
- getState
-) => {
+export const initializeApplication = (): AppThunk => async (dispatch) => {
dispatch(setIsLoading(true));
- dispatch(setNotificationType("info"));
- dispatch(setNotificationMessage(""));
- const state: RootState = getState();
- const { jobsFetchedAt } = state.application;
- // * Establish Job Data
- if (jobsFetchedAt) {
- const isWithinToday = isWithinInterval(new Date(jobsFetchedAt), {
- start: startOfToday(),
- end: endOfToday(),
- });
+ dispatch(displayNotification("", "default"));
- if (!isWithinToday) {
- dispatch(getJobs());
- }
- } else {
- dispatch(getJobs());
- }
+ // * Establish Job Data
+ dispatch(getJobs());
// * Establish User Authentication
dispatch(checkAuthentication());
@@ -237,14 +224,15 @@ export const checkAuthentication = (): AppThunk => async (dispatch) => {
export const logOut = (): AppThunk => async (dispatch) => {
dispatch(setIsLoading(true));
+ // TODO - Modify
const response = await fetchServerData("/user/logout", "POST");
if (response.error) {
console.error(response.error);
- dispatch(setNotificationType("error"));
dispatch(
- setNotificationMessage(
- "Error when attempting to log out. Please try again or contact the developer."
+ displayNotification(
+ "Error when attempting to log out. Please try again or contact the developer.",
+ "error"
)
);
return;
@@ -252,7 +240,7 @@ export const logOut = (): AppThunk => async (dispatch) => {
dispatch(setConfirmPassword(""));
dispatch(setEmail(""));
- dispatch(setNotificationMessage(""));
+ dispatch(displayNotification("", "default"));
dispatch(setName(""));
dispatch(setPassword(""));
dispatch(setSavedJobs([]));
@@ -263,14 +251,15 @@ export const logOut = (): AppThunk => async (dispatch) => {
export const logOutAll = (): AppThunk => async (dispatch) => {
dispatch(setIsLoading(true));
+ // TODO - Modify
const response = await fetchServerData("/user/logout/all", "POST");
if (response.error) {
console.error(response.error);
- dispatch(setNotificationType("error"));
dispatch(
- setNotificationMessage(
- "Error when attempting to log out. Please try again or contact the developer."
+ displayNotification(
+ "Error when attempting to log out. Please try again or contact the developer.",
+ "error"
)
);
return;
@@ -278,7 +267,7 @@ export const logOutAll = (): AppThunk => async (dispatch) => {
dispatch(setConfirmPassword(""));
dispatch(setEmail(""));
- dispatch(setNotificationMessage(""));
+ dispatch(displayNotification("", "default"));
dispatch(setName(""));
dispatch(setPassword(""));
dispatch(setSavedJobs([]));
@@ -289,6 +278,7 @@ export const logOutAll = (): AppThunk => async (dispatch) => {
export const resetPassword = (): AppThunk => async (dispatch, getState) => {
dispatch(setIsLoading(true));
+ dispatch(displayNotification("", "default"));
const state: RootState = getState();
const {
@@ -298,13 +288,13 @@ export const resetPassword = (): AppThunk => async (dispatch, getState) => {
} = state.user;
if (resetConfirmNewPassword !== resetNewPassword) {
- dispatch(setNotificationType("error"));
- dispatch(setNotificationMessage("Passwords do not match."));
+ dispatch(displayNotification("Passwords do not match.", "error"));
dispatch(setIsLoading(false));
return;
}
try {
+ // TODO - Modify
const response: ResetPasswordResponse = await fetchServerData(
"/user/me",
"PATCH",
@@ -315,14 +305,12 @@ export const resetPassword = (): AppThunk => async (dispatch, getState) => {
);
if (response.error) {
- dispatch(setNotificationType("error"));
- dispatch(setNotificationMessage(response.error));
+ dispatch(displayNotification(response.error, "error"));
dispatch(setIsLoading(false));
return;
}
- dispatch(setNotificationType("info"));
- dispatch(setNotificationMessage("Password reset successfully."));
+ dispatch(displayNotification("Password reset successfully.", "success"));
dispatch(setResetConfirmNewPassword(""));
dispatch(setResetCurrentPassword(""));
dispatch(setResetNewPassword(""));
@@ -330,8 +318,7 @@ export const resetPassword = (): AppThunk => async (dispatch, getState) => {
dispatch(setIsLoading(false));
} catch (error) {
console.error(error);
- dispatch(setNotificationType("error"));
- dispatch(setNotificationMessage(error));
+ dispatch(displayNotification(error, "error"));
dispatch(setIsLoading(false));
}
};
@@ -340,7 +327,7 @@ export const cancelResetPassword = (): AppThunk => (dispatch) => {
dispatch(setResetConfirmNewPassword(""));
dispatch(setResetCurrentPassword(""));
dispatch(setResetNewPassword(""));
- dispatch(setNotificationMessage(""));
+ dispatch(displayNotification("", "default"));
dispatch(setIsResettingPassword(false));
};
@@ -349,7 +336,7 @@ export const clickEditProfile = (): AppThunk => (dispatch, getState) => {
const { email, name } = state.user;
- dispatch(setNotificationMessage(""));
+ dispatch(displayNotification("", "default"));
dispatch(setEditEmail(email));
dispatch(setEditName(name));
dispatch(setIsEditingProfile(true));
@@ -358,16 +345,18 @@ export const clickEditProfile = (): AppThunk => (dispatch, getState) => {
export const cancelEditProfile = (): AppThunk => (dispatch) => {
dispatch(setEditEmail(""));
dispatch(setEditName(""));
- dispatch(setNotificationMessage(""));
+ dispatch(displayNotification("", "default"));
dispatch(setIsEditingProfile(false));
};
export const editProfile = (): AppThunk => async (dispatch, getState) => {
dispatch(setIsLoading(true));
+ dispatch(displayNotification("", "default"));
const state: RootState = getState();
const { editEmail, editName } = state.user;
try {
+ // TODO - Modify
const response: EditProfileResponse = await fetchServerData(
"/user/me",
"PATCH",
@@ -375,15 +364,16 @@ export const editProfile = (): AppThunk => async (dispatch, getState) => {
);
if (response.error) {
- dispatch(setNotificationType("error"));
- dispatch(setNotificationMessage(response.error));
+ dispatch(displayNotification(response.error, "error"));
dispatch(setIsLoading(false));
return;
}
- dispatch(setNotificationType("info"));
dispatch(
- setNotificationMessage("Profile information updated successfully.")
+ displayNotification(
+ "Profile information updated successfully.",
+ "success"
+ )
);
dispatch(setEditEmail(""));
dispatch(setEditName(""));
@@ -393,22 +383,21 @@ export const editProfile = (): AppThunk => async (dispatch, getState) => {
dispatch(setIsLoading(false));
} catch (error) {
console.error(error);
- dispatch(setNotificationType("error"));
- dispatch(setNotificationMessage(error));
+ dispatch(displayNotification(error, "error"));
dispatch(setIsLoading(false));
}
};
export const cancelDeleteProfile = (): AppThunk => (dispatch) => {
- dispatch(setNotificationMessage(""));
+ dispatch(displayNotification("", "default"));
dispatch(setIsDeletingProfile(false));
};
export const clickDeleteProfile = (): AppThunk => (dispatch) => {
- dispatch(setNotificationType("warning"));
dispatch(
- setNotificationMessage(
- "Are you sure you would like to delete your profile? This can not be reversed."
+ displayNotification(
+ "Are you sure you would like to delete your profile? This can not be reversed.",
+ "warning"
)
);
dispatch(setIsDeletingProfile(true));
@@ -416,22 +405,22 @@ export const clickDeleteProfile = (): AppThunk => (dispatch) => {
export const deleteProfile = (): AppThunk => async (dispatch) => {
dispatch(setIsLoading(true));
+ dispatch(displayNotification("", "default"));
try {
+ // TODO - Modify
const response: DeleteProfileResponse = await fetchServerData(
"/user/me",
"DELETE"
);
if (response.error) {
- dispatch(setNotificationType("error"));
- dispatch(setNotificationMessage(response.error));
+ dispatch(displayNotification(response.error, "error"));
dispatch(setIsLoading(false));
return;
}
- dispatch(setNotificationType("info"));
- dispatch(setNotificationMessage("Profile deleted successfully."));
+ dispatch(displayNotification("Profile deleted successfully.", "success"));
dispatch(setEmail(""));
dispatch(setName(""));
dispatch(setSavedJobs([]));
@@ -440,76 +429,125 @@ export const deleteProfile = (): AppThunk => async (dispatch) => {
dispatch(setIsLoading(false));
} catch (error) {
console.error(error);
- dispatch(setNotificationType("error"));
- dispatch(setNotificationMessage(error));
+ dispatch(displayNotification(error, "error"));
dispatch(setIsLoading(false));
}
};
-export const addSavedJob = (job: Job): AppThunk => async (dispatch) => {
+export const addSavedJob = (id: string): AppThunk => async (dispatch) => {
dispatch(setIsLoading(true));
try {
- const response: AddSavedJobResponse = await fetchServerData(
+ // TODO - Modify
+ const result:
+ | AddSavedJobErrorResponse
+ | AddSavedJobSuccessResponse = await fetchServerData(
"/user/savedJobs",
"PATCH",
- JSON.stringify({ method: "ADD", job })
+ JSON.stringify({ method: "ADD", id })
);
- if (response.error) {
- dispatch(setNotificationType("error"));
- dispatch(setNotificationMessage(response.error));
+ if (isError(result)) {
+ dispatch(displayNotification(result.error, "error"));
dispatch(setIsLoading(false));
return;
}
- const { savedJobs } = response;
+ const { savedJobs } = result;
dispatch(setSavedJobs(savedJobs));
dispatch(setSavedJobsCurrentPage(1));
dispatch(setSavedJobsTotalPages(Math.ceil(savedJobs.length / 5)));
- dispatch(setNotificationType("info"));
- dispatch(setNotificationMessage("Job saved successfully."));
+ dispatch(displayNotification("Job saved successfully.", "success"));
dispatch(setIsLoading(false));
} catch (error) {
console.error(error);
- dispatch(setNotificationType("error"));
- dispatch(setNotificationMessage(error));
+ dispatch(displayNotification(error, "error"));
dispatch(setIsLoading(false));
}
};
-export const removeSavedJob = (job: Job): AppThunk => async (dispatch) => {
+export const removeSavedJob = (id: string): AppThunk => async (dispatch) => {
dispatch(setIsLoading(true));
try {
- const response: RemoveSavedJobResponse = await fetchServerData(
+ // TODO - Modify
+ const result:
+ | RemoveSavedJobErrorResponse
+ | RemoveSavedJobSuccessResponse = await fetchServerData(
"/user/savedJobs",
"PATCH",
- JSON.stringify({ method: "REMOVE", job })
+ JSON.stringify({ method: "REMOVE", id })
);
- if (response.error) {
- dispatch(setNotificationType("error"));
- dispatch(setNotificationMessage(response.error));
+ if (isError(result)) {
+ dispatch(displayNotification(result.error, "error"));
dispatch(setIsLoading(false));
return;
}
- const { savedJobs } = response;
+ const { savedJobs } = result;
dispatch(setSavedJobs(savedJobs));
dispatch(setSavedJobsCurrentPage(1));
dispatch(setSavedJobsTotalPages(Math.ceil(savedJobs.length / 5)));
- dispatch(setNotificationType("info"));
- dispatch(setNotificationMessage("Job removed successfully."));
+ dispatch(displayNotification("Job removed successfully.", "success"));
dispatch(setIsLoading(false));
} catch (error) {
console.error(error);
- dispatch(setNotificationType("error"));
- dispatch(setNotificationMessage(error));
+ dispatch(displayNotification(error, "error"));
dispatch(setIsLoading(false));
}
};
export const clickViewSavedJobs = (): AppThunk => (dispatch) => {
+ dispatch(displayNotification("", "default"));
dispatch(setIsViewingSavedJobs(true));
};
+
+export const getJobDetails = (id: string): AppThunk => async (dispatch) => {
+ dispatch(setIsLoading(true));
+ dispatch(displayNotification("", "default"));
+
+ try {
+ const result: Job = await fetchServerData(`/jobs/${id}`, "GET");
+
+ if (isError(result)) {
+ dispatch(displayNotification(result.error, "error"));
+ dispatch(setIsLoading(false));
+ return;
+ }
+
+ dispatch(setJobDetails(result));
+ dispatch(setIsLoading(false));
+ } catch (error) {
+ console.error(error);
+ dispatch(displayNotification(error, "error"));
+ dispatch(setIsLoading(false));
+ }
+};
+
+export const getSavedJobsDetails = (): AppThunk => async (dispatch) => {
+ dispatch(setIsLoading(true));
+ dispatch(displayNotification("", "default"));
+
+ try {
+ const result:
+ | GetSavedJobsDetailsErrorResponse
+ | GetSavedJobsDetailsSuccessResponse = await fetchServerData(
+ `/user/savedJobsDetails`,
+ "GET"
+ );
+
+ if (isError(result)) {
+ dispatch(displayNotification(result.error, "error"));
+ dispatch(setIsLoading(false));
+ return;
+ }
+
+ dispatch(setSavedJobsDetails(result));
+ dispatch(setIsLoading(false));
+ } catch (error) {
+ console.error(error);
+ dispatch(displayNotification(error, "error"));
+ dispatch(setIsLoading(false));
+ }
+};
diff --git a/src/client/types.ts b/src/client/types.ts
index 10c3972..eebb3e8 100644
--- a/src/client/types.ts
+++ b/src/client/types.ts
@@ -1,7 +1,19 @@
import { Action } from "redux";
import { ThunkAction } from "redux-thunk";
-export type AddSavedJobResponse = ServerResponseError & ServerResponseUser;
+export interface AddSavedJobErrorResponse {
+ error: string;
+}
+
+export interface AddSavedJobSuccessResponse {
+ createdAt: string;
+ email: string;
+ name: string;
+ savedJobs: string[];
+ updatedAt: string;
+ __v: number;
+ _id: string;
+}
export interface ApplicationAction {
type: string;
@@ -14,8 +26,8 @@ export interface ApplicationState {
currentPage: number;
fullTime: boolean;
isLoading: boolean;
+ jobDetails: Job;
jobs: Job[];
- jobsFetchedAt: string;
locationSearch: string;
notificationMessage: string;
notificationType: NotificationType;
@@ -38,6 +50,24 @@ export type DeleteProfileResponse = ServerResponseError & ServerResponseUser;
export type EditProfileResponse = ServerResponseError & ServerResponseUser;
+export interface GetJobDetailsErrorResponse {
+ error: string;
+}
+
+export type GetJobDetailsSuccessResponse = Job;
+
+export interface GetJobsErrorResponse {
+ error: string;
+}
+
+export type GetJobsSuccessResponse = Job[];
+
+export interface GetSavedJobsDetailsErrorResponse {
+ error: string;
+}
+
+export type GetSavedJobsDetailsSuccessResponse = Job[];
+
export type InputAutoComplete =
| "off"
| "on"
@@ -131,11 +161,29 @@ export interface LocationOption {
export type LoginResponse = ServerResponseError & ServerResponseUser;
-export type NotificationType = "error" | "info" | "warning";
+export type NotificationType =
+ | "error"
+ | "dark"
+ | "default"
+ | "info"
+ | "success"
+ | "warning";
export type PaginationNavigationType = "left" | "right";
-export type RemoveSavedJobResponse = ServerResponseError & ServerResponseUser;
+export interface RemoveSavedJobErrorResponse {
+ error: string;
+}
+
+export interface RemoveSavedJobSuccessResponse {
+ createdAt: string;
+ email: string;
+ name: string;
+ savedJobs: string[];
+ updatedAt: string;
+ __v: number;
+ _id: string;
+}
export type RequestMethod = "DELETE" | "GET" | "PATCH" | "POST";
@@ -156,23 +204,21 @@ export interface ServerResponseUser {
createdAt: string;
email: string;
name: string;
- savedJobs: Job[];
+ savedJobs: string[];
updatedAt: string;
__v: number;
_id: string;
}
-export type SignupResponse = SignupResponseError & SignupResponseSuccess;
-
-export interface SignupResponseError {
+export interface SignupErrorResponse {
error: string;
}
-export interface SignupResponseSuccess {
+export interface SignupSuccessResponse {
createdAt: string;
email: string;
name: string;
- savedJobs: Job[];
+ savedJobs: string[];
updatedAt: string;
__v: number;
_id: string;
@@ -199,7 +245,8 @@ export interface UserState {
resetConfirmNewPassword: string;
resetCurrentPassword: string;
resetNewPassword: string;
- savedJobs: Job[];
+ savedJobs: string[];
savedJobsCurrentPage: number;
+ savedJobsDetails: Job[];
savedJobsTotalPages: number;
}
diff --git a/src/client/util.ts b/src/client/util.ts
index 6c89c27..136b12d 100644
--- a/src/client/util.ts
+++ b/src/client/util.ts
@@ -1,4 +1,14 @@
-import { RequestMethod } from "./types";
+import {
+ RequestMethod,
+ GetJobDetailsErrorResponse,
+ GetJobDetailsSuccessResponse,
+ GetJobsErrorResponse,
+ GetJobsSuccessResponse,
+ AddSavedJobErrorResponse,
+ AddSavedJobSuccessResponse,
+ SignupErrorResponse,
+ SignupSuccessResponse,
+} from "./types";
export const fetchServerData = async (
url: string,
@@ -11,6 +21,9 @@ export const fetchServerData = async (
headers: { "Content-Type": "application/json" },
method,
});
+ if (response.status === 500 || response.status === 404) {
+ return { error: `An error occured. Response Status = ${response.status}` };
+ }
const data = await response.json();
return data;
};
@@ -68,3 +81,17 @@ export const saveState = (state: any): void => {
console.error(error);
}
};
+
+export const isError = (
+ result:
+ | GetJobsErrorResponse
+ | GetJobsSuccessResponse
+ | GetJobDetailsErrorResponse
+ | GetJobDetailsSuccessResponse
+ | AddSavedJobErrorResponse
+ | AddSavedJobSuccessResponse
+ | SignupErrorResponse
+ | SignupSuccessResponse
+): result is GetJobsErrorResponse => {
+ return (result as GetJobsErrorResponse).error !== undefined;
+};
diff --git a/src/server/app.ts b/src/server/app.ts
index 3293fa3..e37edf7 100644
--- a/src/server/app.ts
+++ b/src/server/app.ts
@@ -74,6 +74,10 @@ class App {
this.app.use(express.static(path.join(__dirname, "../dist")));
this.app.get("*", (req: Request, res: Response) => {
+ console.log({ hostname: req.hostname });
+ if (req.hostname === "herokuapp") {
+ return res.status(308).redirect("https://www.githubjobs.io/");
+ }
res.sendFile(path.join(__dirname, "../dist/index.html"));
});
}
diff --git a/src/server/controllers/job.ts b/src/server/controllers/job.ts
index 794e072..5beb7ff 100644
--- a/src/server/controllers/job.ts
+++ b/src/server/controllers/job.ts
@@ -1,9 +1,20 @@
+import endOfToday from "date-fns/endOfToday";
import express, { Request, Response, Router } from "express";
+import isWithinInterval from "date-fns/isWithinInterval";
import nfetch from "node-fetch";
+import startOfToday from "date-fns/startOfToday";
-import { createSearchURL } from "../util";
+import JobModel from "../models/Job";
-import { Job } from "../types";
+import { getAllJobsFromAPI, isError, unique } from "../util";
+
+import {
+ GetJobsErrorResponse,
+ GetJobsSuccessResponse,
+ Job,
+ GetJobDetailsErrorResponse,
+ GetJobDetailsSuccessResponse,
+} from "../types";
/**
* Job Controller.
@@ -16,60 +27,199 @@ class JobController {
}
public initializeRoutes(): void {
- this.router.get("/jobs", async (req: Request, res: Response) => {
- try {
- const jobs: Job[] = [];
- let jobsInBatch = null;
- let page = 1;
-
- // * Can only get 50 jobs at a time
- // * keep going until there are no more jobs
- while (jobsInBatch !== 0) {
- const response = await nfetch(
- `https://jobs.github.com/positions.json?page=${page}`,
- { headers: { "Content-Type": "application/json" }, method: "GET" }
- );
- const batchJobs: Job[] = await response.json();
- jobsInBatch = batchJobs.length;
- page++;
- if (jobsInBatch !== 0) {
- jobs.push(...batchJobs);
+ this.router.get(
+ "/jobs",
+ async (
+ req: Request,
+ res: Response
+ ): Promise> => {
+ try {
+ const currentJobs = await JobModel.find({});
+
+ // * No Jobs exist in DB
+ if (currentJobs.length === 0) {
+ const result = await getAllJobsFromAPI();
+
+ if (isError(result)) {
+ return res.status(500).send(result);
+ }
+
+ await Promise.all(
+ result.map(async (job: Job) => {
+ const newJob = new JobModel(job);
+ await newJob.save();
+ return;
+ })
+ );
+
+ const dbJobs = await JobModel.find({});
+ return res.send(dbJobs);
+ } else {
+ // * Jobs exist in DB
+ const { createdAt } = currentJobs[0];
+
+ const isWithinToday = isWithinInterval(new Date(createdAt), {
+ start: startOfToday(),
+ end: endOfToday(),
+ });
+
+ if (!isWithinToday) {
+ // * Jobs are stale. Get new jobs.
+ const result = await getAllJobsFromAPI();
+
+ if (isError(result)) {
+ return res.status(500).send(result);
+ }
+
+ // * Drop the current database of Jobs
+ await JobModel.collection.drop();
+
+ // * Create new Job entries
+ await Promise.all(
+ result.map(async (job: Job) => {
+ const newJob = new JobModel(job);
+ await newJob.save();
+ return;
+ })
+ );
+
+ const dbJobs = await JobModel.find({});
+ return res.send(dbJobs);
+ } else {
+ // * Jobs are fine, send that.
+ return res.send(currentJobs);
+ }
}
+ } catch (error) {
+ if (process.env.NODE_ENV !== "test") {
+ console.error(error);
+ }
+ res.status(500).send({ error });
}
-
- res.send(jobs);
- } catch (error) {
- res.status(500).send({ error });
}
- });
-
- this.router.get("/jobs/search", async (req: Request, res: Response) => {
- try {
- const { description, full_time, location } = req.query;
- const jobs: Job[] = [];
- let jobsInBatch = null;
- let page = 1;
-
- while (jobsInBatch !== 0) {
- const url = createSearchURL(page, description, full_time, location);
-
- const response = await nfetch(url, {
- headers: { "Content-Type": "application/json" },
- method: "GET",
- });
- const batchJobs: Job[] = await response.json();
- jobsInBatch = batchJobs.length;
- page++;
- if (jobsInBatch !== 0) {
- jobs.push(...batchJobs);
+ );
+
+ // TODO - Optimize (?)
+ this.router.get(
+ "/jobs/search",
+ async (
+ req: Request,
+ res: Response
+ ): Promise> => {
+ try {
+ const {
+ description,
+ full_time,
+ location1,
+ location2,
+ location3,
+ location4,
+ location5,
+ } = req.query;
+
+ const isLocationSearch =
+ location1 || location2 || location3 || location4;
+
+ // * If there is a location in the search, use the API
+ // * If there is not a location, just query the DB
+
+ if (isLocationSearch) {
+ const jobs: Job[] = [];
+ let jobsInBatch = null;
+ let page = 1;
+ const locations = [
+ location1,
+ location2,
+ location3,
+ location4,
+ location5,
+ ];
+
+ await Promise.all(
+ locations.map(async (location: string | undefined) => {
+ if (location) {
+ while (jobsInBatch !== 0) {
+ const url = `https://jobs.github.com/positions.json?page=${page}&description=${encodeURI(
+ description.toString()
+ )}&location=${encodeURI(location)}`;
+
+ const response = await nfetch(url, {
+ headers: { "Content-Type": "application/json" },
+ method: "GET",
+ });
+ const batchJobs: Job[] = await response.json();
+ jobsInBatch = batchJobs.length;
+ page++;
+ if (jobsInBatch !== 0) {
+ jobs.push(...batchJobs);
+ }
+ }
+ }
+ })
+ );
+
+ const uniqueResults: Job[] = unique(jobs);
+
+ return res.send(uniqueResults);
}
+
+ // * Make Searches
+ const regexSearch = new RegExp(description.toString(), "i");
+
+ const companyQuery = JobModel.find({ company: regexSearch });
+ const descriptionQuery = JobModel.find({ description: regexSearch });
+ const titleQuery = JobModel.find({ title: regexSearch });
+
+ if (full_time === "true") {
+ companyQuery.find({ type: "Full Time" });
+ descriptionQuery.find({ type: "Full Time" });
+ titleQuery.find({ type: "Full Time" });
+ }
+
+ const companyResults = await companyQuery.exec();
+ const descriptionResults = await descriptionQuery.exec();
+ const titleResults = await titleQuery.exec();
+
+ // * Combine search results into 1 array
+ const searchResults: Job[] = [
+ ...companyResults,
+ ...descriptionResults,
+ ...titleResults,
+ ];
+
+ const uniqueResults: Job[] = unique(searchResults);
+
+ return res.send(uniqueResults);
+ } catch (error) {
+ if (process.env.NODE_ENV !== "test") {
+ console.error(error);
+ }
+ res.status(500).send({ error });
}
+ }
+ );
+
+ this.router.get(
+ "/jobs/:id",
+ async (
+ req: Request,
+ res: Response
+ ): Promise<
+ Response
+ > => {
+ try {
+ const { id } = req.params;
+ const jobDetails = await JobModel.findOne({ id });
- res.send(jobs);
- } catch (error) {
- res.status(500).send({ error });
+ return res.send(jobDetails);
+ } catch (error) {
+ if (process.env.NODE_ENV !== "test") {
+ console.error(error);
+ }
+ res.status(500).send({ error });
+ }
}
- });
+ );
}
}
diff --git a/src/server/controllers/user.ts b/src/server/controllers/user.ts
index 7795a94..e2e86b9 100644
--- a/src/server/controllers/user.ts
+++ b/src/server/controllers/user.ts
@@ -5,14 +5,19 @@ import validator from "validator";
import auth from "../middleware/auth";
+import JobModel from "../models/Job";
import User from "../models/User";
import {
AuthenticatedRequest,
EditSavedJobsMethod,
- Job,
+ GetSavedJobsDetailsErrorResponse,
+ GetSavedJobsDetailsSuccessResponse,
+ PatchSavedJobErrorResponse,
+ PatchSavedJobSuccessResponse,
Token,
UserDocument,
+ Job,
} from "../types";
/**
@@ -195,11 +200,16 @@ class UserController {
this.router.patch(
"/user/savedJobs",
auth,
- async (req: AuthenticatedRequest, res: Response) => {
+ async (
+ req: AuthenticatedRequest,
+ res: Response
+ ): Promise<
+ Response
+ > => {
try {
const method: EditSavedJobsMethod = req.body.method;
- const job: Job = req.body.job;
- const currentSavedJobs = req.user.savedJobs;
+ const id: string = req.body.id;
+ const currentSavedJobs: string[] = req.user.savedJobs;
let newJobs;
if (method !== "ADD" && method !== "REMOVE") {
@@ -210,11 +220,11 @@ class UserController {
if (method === "ADD") {
// * User is attempting to add a saved job
- newJobs = [...currentSavedJobs, job];
+ newJobs = [...currentSavedJobs, id];
} else if (method === "REMOVE") {
// * User is attempting to remove a saved job
newJobs = currentSavedJobs.filter(
- (savedJob: Job) => savedJob.id !== job.id
+ (savedJobID: string) => savedJobID !== id
);
}
req.user.savedJobs = newJobs;
@@ -230,6 +240,50 @@ class UserController {
}
);
+ this.router.get(
+ "/user/savedJobsDetails",
+ auth,
+ async (
+ req: AuthenticatedRequest,
+ res: Response
+ ): Promise<
+ Response<
+ GetSavedJobsDetailsErrorResponse | GetSavedJobsDetailsSuccessResponse
+ >
+ > => {
+ try {
+ const { savedJobs } = req.user;
+
+ const savedJobsDetails: Job[] = [];
+ let dbError = false;
+
+ await Promise.all(
+ savedJobs.map(async (id: string) => {
+ const job = await JobModel.findOne({ id });
+
+ if (!job) {
+ return (dbError = true);
+ }
+ return savedJobsDetails.push(job);
+ })
+ );
+
+ if (dbError) {
+ return res
+ .status(500)
+ .send({ error: "Error finding corresponding jobs in database." });
+ }
+
+ return res.send(savedJobsDetails);
+ } catch (error) {
+ if (process.env.NODE_ENV !== "test") {
+ console.error(error);
+ }
+ return res.status(500).send({ error });
+ }
+ }
+ );
+
this.router.patch(
"/user/me",
auth,
@@ -273,19 +327,6 @@ class UserController {
// * Send User as respoonse
return res.send(req.user);
} catch (error) {
- if (error.errors.password) {
- // * Min Length Validation Error
- if (error.errors.password.kind === "minlength") {
- return res.status(400).send({
- error: "Password must be a minimum of 7 characters.",
- });
- }
- // * Password Validation Error
- return res
- .status(400)
- .send({ error: error.errors.password.message });
- }
-
if (process.env.NODE_ENV !== "test") {
console.error(error);
}
diff --git a/src/server/models/Job.ts b/src/server/models/Job.ts
new file mode 100644
index 0000000..ad69ed4
--- /dev/null
+++ b/src/server/models/Job.ts
@@ -0,0 +1,65 @@
+import mongoose from "mongoose";
+
+import { JobDocument } from "../types";
+
+const jobSchema = new mongoose.Schema(
+ {
+ company: {
+ required: [true, "Field 'company' is required."],
+ type: String,
+ },
+ company_logo: {
+ required: false,
+ type: String,
+ },
+ company_url: {
+ required: false,
+ type: String,
+ },
+ created_at: {
+ required: [true, "Field 'created_at' is required."],
+ type: String,
+ },
+ description: {
+ required: [true, "Field 'description' is required."],
+ type: String,
+ },
+ how_to_apply: {
+ required: [true, "Field 'how_to_apply' is required."],
+ type: String,
+ },
+ id: {
+ required: [true, "Field 'id' is required."],
+ type: String,
+ },
+ location: {
+ required: [true, "Field 'location' is required."],
+ type: String,
+ },
+ title: {
+ required: [true, "Field 'title' is required."],
+ type: String,
+ },
+ type: {
+ required: [true, "Field 'type' is required."],
+ type: String,
+ },
+ url: {
+ required: [true, "Field 'url' is required."],
+ type: String,
+ },
+ },
+ { timestamps: true }
+);
+
+function contentToJSON(): void {
+ const jobsObj = this.toObject();
+
+ return jobsObj;
+}
+
+jobSchema.methods.toJSON = contentToJSON;
+
+const Job = mongoose.model("Job", jobSchema);
+
+export default Job;
diff --git a/src/server/models/User.ts b/src/server/models/User.ts
index ff42a23..3e503c8 100644
--- a/src/server/models/User.ts
+++ b/src/server/models/User.ts
@@ -79,17 +79,7 @@ const userSchema = new mongoose.Schema(
},
savedJobs: [
{
- company: String,
- company_logo: String,
- company_url: String,
- created_at: String,
- description: String,
- how_to_apply: String,
- id: String,
- location: String,
- title: String,
- type: { type: String },
- url: String,
+ type: String,
},
],
tokens: [
diff --git a/src/server/types.ts b/src/server/types.ts
index 25aff27..b5f9437 100644
--- a/src/server/types.ts
+++ b/src/server/types.ts
@@ -1,5 +1,6 @@
import { Request, Router } from "express";
import { Document, Model } from "mongoose";
+import Job from "./models/Job";
export interface AuthenticatedRequest extends Request {
token: string;
@@ -12,6 +13,24 @@ export type Controller = {
export type EditSavedJobsMethod = "ADD" | "REMOVE";
+export interface GetJobDetailsErrorResponse {
+ error: string;
+}
+
+export type GetJobDetailsSuccessResponse = Job;
+
+export interface GetJobsErrorResponse {
+ error: string;
+}
+
+export type GetJobsSuccessResponse = Job[];
+
+export interface GetSavedJobsDetailsErrorResponse {
+ error: string;
+}
+
+export type GetSavedJobsDetailsSuccessResponse = Job[];
+
export interface Job {
company: string;
company_logo: string;
@@ -26,8 +45,40 @@ export interface Job {
url: string;
}
+export interface JobDocument extends Document {
+ _id: string;
+ company: string;
+ company_logo: string;
+ company_url: string;
+ created_at: string;
+ description: string;
+ how_to_apply: string;
+ id: string;
+ location: string;
+ title: string;
+ type: JobType;
+ url: string;
+ createdAt: string;
+ updatedAt: string;
+ __v: number;
+}
+
export type JobType = "Contract" | "Full Time";
+export interface PatchSavedJobErrorResponse {
+ error: string;
+}
+
+export interface PatchSavedJobSuccessResponse {
+ createdAt: string;
+ email: string;
+ name: string;
+ savedJobs: string[];
+ updatedAt: string;
+ __v: number;
+ _id: string;
+}
+
export interface Token {
_id: string;
token: string;
@@ -39,7 +90,7 @@ export interface UserDocument extends Document {
generateAuthToken(): Promise;
password: string;
name: string;
- savedJobs: Job[];
+ savedJobs: string[];
tokens: Token[];
}
diff --git a/src/server/util.ts b/src/server/util.ts
index b5a60a4..1dfac9b 100644
--- a/src/server/util.ts
+++ b/src/server/util.ts
@@ -1,4 +1,6 @@
-import fetch from "node-fetch";
+import nfetch from "node-fetch";
+
+import { GetJobsErrorResponse, GetJobsSuccessResponse, Job } from "./types";
/**
* Check if MongoDB is running locally. Stops application from continuing if false.
@@ -41,3 +43,42 @@ export const createSearchURL = (
return url;
};
+
+export const getAllJobsFromAPI = async (): Promise<
+ GetJobsErrorResponse | GetJobsSuccessResponse
+> => {
+ const jobs: Job[] = [];
+ let jobsInBatch = null;
+ let page = 1;
+
+ // * Can only get 50 jobs at a time
+ // * keep going until there are no more jobs
+ try {
+ while (jobsInBatch !== 0) {
+ const response = await nfetch(
+ `https://jobs.github.com/positions.json?page=${page}`,
+ { headers: { "Content-Type": "application/json" }, method: "GET" }
+ );
+ const batchJobs: Job[] = await response.json();
+ jobsInBatch = batchJobs.length;
+ page++;
+ if (jobsInBatch !== 0) {
+ jobs.push(...batchJobs);
+ }
+ }
+
+ return jobs;
+ } catch (error) {
+ console.error(error);
+ return { error };
+ }
+};
+
+export const isError = (
+ result: GetJobsErrorResponse | GetJobsSuccessResponse
+): result is GetJobsErrorResponse => {
+ return (result as GetJobsErrorResponse).error !== undefined;
+};
+
+// eslint-disable-next-line
+export const unique = (arr: any[]): any[] => [...new Set(arr)];