frontend converted to ts
This commit is contained in:
@@ -1,4 +1 @@
|
||||
node_modules
|
||||
default.json
|
||||
.vscode
|
||||
TODO.txt
|
||||
|
||||
+13
-4
@@ -4,15 +4,22 @@
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@reduxjs/toolkit": "^1.9.5",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/jest-dom": "^5.17.0",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/node": "^16.18.39",
|
||||
"@types/react": "^18.2.16",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"axios": "^1.4.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-redux": "^8.0.5",
|
||||
"react-router-dom": "^6.11.2",
|
||||
"react-redux": "^8.1.1",
|
||||
"react-router-dom": "^6.14.2",
|
||||
"react-scripts": "5.0.1",
|
||||
"redux": "^4.2.1",
|
||||
"typescript": "^4.9.5",
|
||||
"uuid": "^9.0.0",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
@@ -39,5 +46,7 @@
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"proxy": "http://localhost:5000"
|
||||
"devDependencies": {
|
||||
"@types/uuid": "^9.0.2"
|
||||
}
|
||||
}
|
||||
|
||||
Executable
+96
@@ -0,0 +1,96 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
|
||||
|
||||
import Navbar from './components/layout/Navbar';
|
||||
import Landing from './components/layout/Landing';
|
||||
import Register from './components/auth/Register';
|
||||
import Login from './components/auth/Login';
|
||||
import Alert from './components/layout/Alert';
|
||||
import Dashboard from './components/dashboard/Dashboard';
|
||||
import ProfileForm from './components/profile-forms/ProfileForm';
|
||||
import AddExperience from './components/profile-forms/AddExperience';
|
||||
import AddEducation from './components/profile-forms/AddEducation';
|
||||
import Profiles from './components/profiles/Profiles';
|
||||
import Profile from './components/profile/Profile';
|
||||
import Posts from './components/posts/Posts';
|
||||
import Post from './components/post/Post';
|
||||
import NotFound from './components/layout/NotFound';
|
||||
import PrivateRoute from './components/routing/PrivateRoute';
|
||||
|
||||
// Redux
|
||||
import { Provider } from 'react-redux';
|
||||
import store from './store';
|
||||
import { loadUser } from './actions/auth';
|
||||
import setAuthToken from './utils/setAuthToken';
|
||||
|
||||
import './App.css';
|
||||
import { logOut } from './reducers/auth';
|
||||
|
||||
const App = () => {
|
||||
useEffect(() => {
|
||||
// check for token in LS when app first runs
|
||||
if (localStorage.token) {
|
||||
// if there is a token set axios headers for all requests
|
||||
setAuthToken(localStorage.token);
|
||||
}
|
||||
// try to fetch a user, if no token or invalid token we
|
||||
// will get a 401 response from our API
|
||||
store.dispatch(loadUser());
|
||||
|
||||
// log user out from all tabs if they log out in one tab
|
||||
window.addEventListener("storage", () => {
|
||||
if (!localStorage.token) store.dispatch(logOut);
|
||||
});
|
||||
}, []);
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<Router>
|
||||
<Navbar />
|
||||
<Alert />
|
||||
<Routes>
|
||||
<Route path="/" element={<Landing />} />
|
||||
<Route path="register" element={<Register />} />
|
||||
<Route path="login" element={<Login />} />
|
||||
<Route path="profiles" element={<Profiles />} />
|
||||
<Route path="profile/:id" element={<Profile />} />
|
||||
<Route
|
||||
path="dashboard"
|
||||
element={<PrivateRoute component={Dashboard} />}
|
||||
/>
|
||||
<Route
|
||||
path="create-profile"
|
||||
element={<PrivateRoute component={ProfileForm} />}
|
||||
/>
|
||||
<Route
|
||||
path="edit-profile"
|
||||
element={<PrivateRoute component={ProfileForm} />}
|
||||
/>
|
||||
<Route
|
||||
path="add-experience"
|
||||
element={<PrivateRoute component={AddExperience} />}
|
||||
/>
|
||||
<Route
|
||||
path="add-education"
|
||||
element={<PrivateRoute component={AddEducation} />}
|
||||
/>
|
||||
<Route path="posts" element={<PrivateRoute component={Posts} />} />
|
||||
<Route path="posts/:id" element={<PrivateRoute component={Post} />} />
|
||||
<Route path="/*" element={<NotFound />} />
|
||||
</Routes>
|
||||
</Router>
|
||||
</Provider>
|
||||
);
|
||||
/*
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="login" element={<Login />} />
|
||||
</Routes>
|
||||
</Router>
|
||||
</Provider>
|
||||
);
|
||||
*/
|
||||
};
|
||||
|
||||
export default App;
|
||||
Executable
+10
@@ -0,0 +1,10 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { removeAlert, setAlert } from '../reducers/alert';
|
||||
import { AppThunk } from '../types';
|
||||
|
||||
export const createAlert = (msg: string, alertType: string, timeout = 5000): AppThunk => dispatch => {
|
||||
const id = uuidv4();
|
||||
dispatch(setAlert({ msg, alertType, id }));
|
||||
|
||||
setTimeout(() => dispatch(removeAlert(id)), timeout);
|
||||
};
|
||||
Executable
+74
@@ -0,0 +1,74 @@
|
||||
import api from "../utils/api";
|
||||
import { createAlert } from "./alert";
|
||||
import {
|
||||
loginSucces,
|
||||
authError,
|
||||
registerSuccess,
|
||||
logOut,
|
||||
userLoaded,
|
||||
} from "../reducers/auth";
|
||||
import { AppThunk } from "../types";
|
||||
import { AxiosError, isAxiosError } from "axios";
|
||||
|
||||
export const login = (email: string, password: string): AppThunk => async (dispatch) => {
|
||||
const body = { email, password };
|
||||
try {
|
||||
const res = await api.post("/auth", body);
|
||||
|
||||
dispatch(loginSucces(res.data));
|
||||
|
||||
dispatch(loadUser());
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof AxiosError || err instanceof Error) {
|
||||
if (isAxiosError(err) && err.response !== undefined) {
|
||||
const errors = err.response.data.errors;
|
||||
if (errors) {
|
||||
errors.forEach((error: any) => dispatch(createAlert(error.msg, "danger")));
|
||||
}
|
||||
dispatch({
|
||||
type: "auth/loginFail",
|
||||
});
|
||||
}
|
||||
}
|
||||
//normal err
|
||||
}
|
||||
// not error
|
||||
}
|
||||
|
||||
export const loadUser = (): AppThunk => async (dispatch) => {
|
||||
try {
|
||||
const res = await api.get("/auth");
|
||||
|
||||
dispatch(userLoaded(res.data));
|
||||
} catch (err) {
|
||||
dispatch(authError());
|
||||
}
|
||||
};
|
||||
|
||||
// Register User
|
||||
export const register = (formData: { name: string, email: string, password: string }): AppThunk => async (dispatch) => {
|
||||
try {
|
||||
const res = await api.post("/users", formData);
|
||||
|
||||
dispatch(registerSuccess(res.data));
|
||||
dispatch(loadUser());
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof AxiosError) {
|
||||
if (err !== undefined && 'response' in err && err.response !== undefined) {
|
||||
const errors = err.response.data.errors;
|
||||
|
||||
if (errors) {
|
||||
errors.forEach((error: any) => dispatch(createAlert(error.msg, "danger")));
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: "auth/registerFail",
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const logout = (): AppThunk => async (dispatch) => {
|
||||
dispatch(logOut);
|
||||
};
|
||||
Executable
+157
@@ -0,0 +1,157 @@
|
||||
import {
|
||||
removeComment,
|
||||
addCommentAction,
|
||||
updateLikes,
|
||||
postError,
|
||||
deletePostAction,
|
||||
getPostAction,
|
||||
getPostsAction,
|
||||
addPostAction,
|
||||
} from "../reducers/post";
|
||||
import { AppThunk } from "../types";
|
||||
import api from "../utils/api";
|
||||
import { createAlert } from "./alert";
|
||||
import { AxiosError } from "axios";
|
||||
|
||||
// Get posts
|
||||
export const getPosts = (): AppThunk => async (dispatch) => {
|
||||
try {
|
||||
const res = await api.get("/posts");
|
||||
|
||||
dispatch(getPostsAction(res.data));
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof AxiosError) {
|
||||
if (err !== undefined && 'response' in err && err.response !== undefined) {
|
||||
dispatch(
|
||||
postError({ msg: err.response.statusText, status: err.response.status })
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Add like
|
||||
export const addLike = (id: string): AppThunk => async (dispatch) => {
|
||||
try {
|
||||
const res = await api.put(`/posts/like/${id}`);
|
||||
|
||||
dispatch(updateLikes({ id, likes: res.data }));
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof AxiosError) {
|
||||
if (err !== undefined && 'response' in err && err.response !== undefined) {
|
||||
dispatch(
|
||||
postError({ msg: err.response.statusText, status: err.response.status })
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Remove like
|
||||
export const removeLike = (id: string): AppThunk => async (dispatch) => {
|
||||
try {
|
||||
const res = await api.put(`/posts/unlike/${id}`);
|
||||
|
||||
dispatch(updateLikes({ id, likes: res.data }));
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof AxiosError) {
|
||||
if (err !== undefined && 'response' in err && err.response !== undefined) {
|
||||
dispatch(
|
||||
postError({ msg: err.response.statusText, status: err.response.status })
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Delete post
|
||||
export const deletePost = (id: string): AppThunk => async (dispatch) => {
|
||||
try {
|
||||
await api.delete(`/posts/${id}`);
|
||||
|
||||
dispatch(deletePostAction(id));
|
||||
|
||||
dispatch(createAlert("Post Removed", "success"));
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof AxiosError) {
|
||||
if (err !== undefined && 'response' in err && err.response !== undefined) {
|
||||
dispatch(
|
||||
postError({ msg: err.response.statusText, status: err.response.status })
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
// Add post
|
||||
export const addPost = (formData: { text: string }): AppThunk => async (dispatch) => {
|
||||
try {
|
||||
const res = await api.post("/posts", formData);
|
||||
|
||||
dispatch(addPostAction(res.data));
|
||||
|
||||
dispatch(createAlert("Post Created", "success"));
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof AxiosError) {
|
||||
if (err !== undefined && 'response' in err && err.response !== undefined) {
|
||||
dispatch(
|
||||
postError({ msg: err.response.statusText, status: err.response.status })
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Get post
|
||||
export const getPost = (id: string): AppThunk => async (dispatch) => {
|
||||
try {
|
||||
const res = await api.get(`/posts/${id}`);
|
||||
|
||||
dispatch(getPostAction(res.data));
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof AxiosError) {
|
||||
if (err !== undefined && 'response' in err && err.response !== undefined) {
|
||||
dispatch(
|
||||
postError({ msg: err.response.statusText, status: err.response.status })
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
// Add comment
|
||||
export const addComment = (postId: string, formData: { text: string }): AppThunk => async (dispatch) => {
|
||||
try {
|
||||
const res = await api.post(`/posts/comment/${postId}`, formData);
|
||||
|
||||
dispatch(addCommentAction(res.data));
|
||||
|
||||
dispatch(createAlert("Comment Added", "success"));
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof AxiosError) {
|
||||
if (err !== undefined && 'response' in err && err.response !== undefined) {
|
||||
dispatch(
|
||||
postError({ msg: err.response.statusText, status: err.response.status })
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
// Delete comment
|
||||
export const deleteComment = (postId: string, commentId: string): AppThunk => async (dispatch) => {
|
||||
try {
|
||||
await api.delete(`/posts/comment/${postId}/${commentId}`);
|
||||
|
||||
dispatch(removeComment(commentId));
|
||||
|
||||
dispatch(createAlert("Comment Removed", "success"));
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof AxiosError) {
|
||||
if (err !== undefined && 'response' in err && err.response !== undefined) {
|
||||
dispatch(
|
||||
postError({ msg: err.response.statusText, status: err.response.status })
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
Executable
+211
@@ -0,0 +1,211 @@
|
||||
import api from "../utils/api";
|
||||
import { createAlert } from "./alert";
|
||||
import {
|
||||
noRepos,
|
||||
getRepos,
|
||||
clearProfile,
|
||||
profileError,
|
||||
getProfilesType,
|
||||
updateProfile,
|
||||
getProfile,
|
||||
} from "../reducers/profile";
|
||||
import { accountDeleted } from "../reducers/auth";
|
||||
import { AxiosError } from "axios";
|
||||
import { AppThunk, EducationType, ExperienceType } from "../types";
|
||||
|
||||
const errorHandle = (dispatch: any, err: unknown) => {
|
||||
if (err instanceof AxiosError) {
|
||||
if (err !== undefined && 'response' in err && err.response !== undefined) {
|
||||
dispatch(profileError({ msg: err.response.statusText, status: err.response.status, }), err)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Get current users profile
|
||||
export const getCurrentProfile = (): AppThunk<Promise<void>> => async (dispatch) => {
|
||||
try {
|
||||
const res = await api.get("/profile/me");
|
||||
|
||||
dispatch(getProfile(res.data));
|
||||
} catch (err: unknown) {
|
||||
errorHandle(dispatch, err)
|
||||
|
||||
}
|
||||
}
|
||||
// Get all profiles
|
||||
export const getProfiles = (): AppThunk<Promise<void>> => async (dispatch) => {
|
||||
dispatch(clearProfile());
|
||||
try {
|
||||
const res = await api.get("/profile");
|
||||
|
||||
dispatch(getProfilesType(res.data));
|
||||
} catch (err: unknown) {
|
||||
errorHandle(dispatch, err)
|
||||
}
|
||||
};
|
||||
|
||||
// Get profile by ID
|
||||
export const getProfileById = (userId: string): AppThunk<Promise<void>> => async (dispatch) => {
|
||||
try {
|
||||
const res = await api.get(`/profile/user/${userId}`);
|
||||
|
||||
dispatch(getProfile(res.data));
|
||||
} catch (err: unknown) {
|
||||
errorHandle(dispatch, err)
|
||||
}
|
||||
};
|
||||
|
||||
// Get Github repos
|
||||
export const getGithubRepos = (username: string): AppThunk<Promise<void>> => async (dispatch) => {
|
||||
try {
|
||||
const res = await api.get(`/profile/github/${username}`);
|
||||
|
||||
dispatch(getRepos(res.data));
|
||||
} catch (err: unknown) {
|
||||
dispatch(noRepos());
|
||||
}
|
||||
};
|
||||
|
||||
// Create or update profile
|
||||
type FormDataType = {
|
||||
company: string,
|
||||
website: string,
|
||||
location: string,
|
||||
status: string,
|
||||
skills: string,
|
||||
githubusername: string,
|
||||
bio: string,
|
||||
twitter: string,
|
||||
facebook: string,
|
||||
linkedin: string,
|
||||
youtube: string,
|
||||
instagram: string,
|
||||
}
|
||||
export const createProfile =
|
||||
(formData: FormDataType, edit = false): AppThunk<Promise<void>> =>
|
||||
async (dispatch) => {
|
||||
try {
|
||||
const res = await api.post("/profile", formData);
|
||||
|
||||
dispatch(getProfile(res.data));
|
||||
|
||||
dispatch(
|
||||
createAlert(edit ? "Profile Updated" : "Profile Created", "success")
|
||||
);
|
||||
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof AxiosError) {
|
||||
if (err !== undefined && 'response' in err && err.response !== undefined) {
|
||||
const errors = err.response.data.errors;
|
||||
if (errors) {
|
||||
errors.forEach((error: any) => dispatch(createAlert(error.msg, "danger")));
|
||||
}
|
||||
dispatch(
|
||||
profileError({
|
||||
msg: err.response.statusText,
|
||||
status: err.response.status,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Add Experience
|
||||
export const addExperience = (formData: Omit<ExperienceType, '_id'>): AppThunk<Promise<void>> => async (dispatch) => {
|
||||
try {
|
||||
const res = await api.put("/profile/experience", formData);
|
||||
|
||||
dispatch(updateProfile(res.data));
|
||||
|
||||
dispatch(createAlert("Experience Added", "success"));
|
||||
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof AxiosError) {
|
||||
if (err !== undefined && 'response' in err && err.response !== undefined) {
|
||||
const errors = err.response.data.errors;
|
||||
|
||||
if (errors) {
|
||||
errors.forEach((error: any) => dispatch(createAlert(error.msg, "danger")));
|
||||
}
|
||||
|
||||
dispatch(
|
||||
profileError({
|
||||
msg: err.response.statusText,
|
||||
status: err.response.status,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Add Education
|
||||
export const addEducation = (formData: Omit<EducationType, '_id'>): AppThunk<Promise<void>> => async (dispatch) => {
|
||||
try {
|
||||
const res = await api.put("/profile/education", formData);
|
||||
|
||||
dispatch(updateProfile(res.data));
|
||||
|
||||
dispatch(createAlert("Education Added", "success"));
|
||||
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof AxiosError) {
|
||||
if (err !== undefined && 'response' in err && err.response !== undefined) {
|
||||
const errors = err.response.data.errors;
|
||||
|
||||
if (errors) {
|
||||
errors.forEach((error: any) => dispatch(createAlert(error.msg, "danger")));
|
||||
}
|
||||
|
||||
dispatch(
|
||||
profileError({
|
||||
msg: err.response.statusText,
|
||||
status: err.response.status,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Delete experience
|
||||
export const deleteExperience = (id: string): AppThunk<Promise<void>> => async (dispatch) => {
|
||||
try {
|
||||
const res = await api.delete(`/profile/experience/${id}`);
|
||||
|
||||
dispatch(updateProfile(res.data));
|
||||
|
||||
dispatch(createAlert("Experience Removed", "success"));
|
||||
} catch (err: unknown) {
|
||||
errorHandle(dispatch, err)
|
||||
}
|
||||
};
|
||||
|
||||
// Delete education
|
||||
export const deleteEducation = (id: string): AppThunk<Promise<void>> => async (dispatch) => {
|
||||
try {
|
||||
const res = await api.delete(`/profile/education/${id}`);
|
||||
|
||||
dispatch(updateProfile(res.data));
|
||||
|
||||
dispatch(createAlert("Education Removed", "success"));
|
||||
} catch (err: unknown) {
|
||||
errorHandle(dispatch, err)
|
||||
}
|
||||
};
|
||||
|
||||
// Delete account & profile
|
||||
export const deleteAccount = (): AppThunk<Promise<void>> => async (dispatch) => {
|
||||
if (window.confirm("Are you sure? This can NOT be undone!")) {
|
||||
try {
|
||||
await api.delete("/profile");
|
||||
|
||||
dispatch(clearProfile());
|
||||
dispatch(accountDeleted());
|
||||
|
||||
dispatch(createAlert("Your account has been permanently deleted", "danger"));
|
||||
} catch (err: unknown) {
|
||||
errorHandle(dispatch, err)
|
||||
}
|
||||
}
|
||||
};
|
||||
Executable
+62
@@ -0,0 +1,62 @@
|
||||
import React, { useState } from "react";
|
||||
import { Link, Navigate } from "react-router-dom";
|
||||
import { useAppDispatch, useAppSelector } from "../../utils/hooks";
|
||||
import { login } from "../../actions/auth";
|
||||
|
||||
const Login = () => {
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const dispatch = useAppDispatch();
|
||||
const isAuthenticated = useAppSelector((state) => state.auth.isAuthenticated);
|
||||
|
||||
const onChangeEmail = () =>
|
||||
setEmail(email)
|
||||
const onChangePasword = () =>
|
||||
setPassword(password)
|
||||
|
||||
const onSubmit = async (e: React.SyntheticEvent) => {
|
||||
e.preventDefault();
|
||||
await dispatch(login(email, password));
|
||||
};
|
||||
|
||||
if (isAuthenticated) {
|
||||
return <Navigate to="/dashboard" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="container">
|
||||
<h1 className="large text-primary">Sign In</h1>
|
||||
<p className="lead">
|
||||
<i className="fas fa-user" /> Sign Into Your Account
|
||||
</p>
|
||||
<form className="form" onSubmit={onSubmit}>
|
||||
<div className="form-group">
|
||||
<input
|
||||
type="email"
|
||||
placeholder="Email Address"
|
||||
name="email"
|
||||
value={email}
|
||||
onChange={onChangeEmail}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<input
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
name="password"
|
||||
value={password}
|
||||
onChange={onChangePasword}
|
||||
//used to be "6"
|
||||
minLength={6}
|
||||
/>
|
||||
</div>
|
||||
<input type="submit" className="btn btn-primary" value="Login" />
|
||||
</form>
|
||||
<p className="my-1">
|
||||
Don't have an account? <Link to="/register">Sign Up</Link>
|
||||
</p>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||
Executable
+86
@@ -0,0 +1,86 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useAppDispatch, useAppSelector } from '../../utils/hooks';
|
||||
import { Link, Navigate } from 'react-router-dom';
|
||||
import { createAlert } from '../../actions/alert';
|
||||
import { register } from '../../actions/auth';
|
||||
|
||||
|
||||
const Register = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const [name, setName] = useState('')
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [password2, setPassword2] = useState('')
|
||||
|
||||
const isAuthenticated = useAppSelector(state => state.auth.isAuthenticated)
|
||||
|
||||
const onSubmit = async (e: React.SyntheticEvent) => {
|
||||
e.preventDefault();
|
||||
if (password !== password2) {
|
||||
dispatch(createAlert('Passwords do not match', 'danger'));
|
||||
} else {
|
||||
dispatch(register({ name, email, password }));
|
||||
}
|
||||
};
|
||||
|
||||
if (isAuthenticated) {
|
||||
return <Navigate to="/dashboard" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="container">
|
||||
<h1 className="large text-primary">Sign Up</h1>
|
||||
<p className="lead">
|
||||
<i className="fas fa-user" /> Create Your Account
|
||||
</p>
|
||||
<form className="form" onSubmit={onSubmit}>
|
||||
<div className="form-group">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Name"
|
||||
name="name"
|
||||
value={name}
|
||||
onChange={() => setName(name)}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<input
|
||||
type="email"
|
||||
placeholder="Email Address"
|
||||
name="email"
|
||||
value={email}
|
||||
onChange={() => setEmail(email)}
|
||||
/>
|
||||
<small className="form-text">
|
||||
This site uses Gravatar so if you want a profile image, use a
|
||||
Gravatar email
|
||||
</small>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<input
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
name="password"
|
||||
value={password}
|
||||
onChange={() => setPassword(password)}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<input
|
||||
type="password"
|
||||
placeholder="Confirm Password"
|
||||
name="password2"
|
||||
value={password2}
|
||||
onChange={() => setPassword2(password2)}
|
||||
/>
|
||||
</div>
|
||||
<input type="submit" className="btn btn-primary" value="Register" />
|
||||
</form>
|
||||
<p className="my-1">
|
||||
Already have an account? <Link to="/login">Sign In</Link>
|
||||
</p>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Register;
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useAppDispatch, useAppSelector } from '../../utils/hooks';
|
||||
import DashboardActions from "./DashboardActions";
|
||||
import Experience from "./Experience";
|
||||
import Education from "./Education";
|
||||
import { getCurrentProfile, deleteAccount } from "../../actions/profile";
|
||||
|
||||
const Dashboard = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
useEffect(() => {
|
||||
function fetchData() {
|
||||
dispatch(getCurrentProfile());
|
||||
}
|
||||
fetchData();
|
||||
}, [dispatch]);
|
||||
const user = useAppSelector((state) => state.auth.user);
|
||||
const profile = useAppSelector((state) => state.profile.profile);
|
||||
return (
|
||||
<section className="container">
|
||||
<h1 className="large text-primary">Dashboard</h1>
|
||||
<p className="lead">
|
||||
<i className="fas fa-user" /> Welcome {user && user.name}
|
||||
</p>
|
||||
{profile !== null ? (
|
||||
<>
|
||||
<DashboardActions />
|
||||
<Experience experience={profile.experience} />
|
||||
<Education education={profile.education} />
|
||||
|
||||
<div className="my-2">
|
||||
<button
|
||||
className="btn btn-danger"
|
||||
|
||||
onClick={async () => await dispatch(deleteAccount())}
|
||||
>
|
||||
<i className="fas fa-user" /> Delete My Account
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p>You have not yet setup a profile, please add some info</p>
|
||||
<Link to="/create-profile" className="btn btn-primary my-1">
|
||||
Create Profile
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
const DashboardActions = () => {
|
||||
return (
|
||||
<div className='dash-buttons'>
|
||||
<Link to='/edit-profile' className='btn btn-light'>
|
||||
<i className='fas fa-user-circle text-primary' /> Edit Profile
|
||||
</Link>
|
||||
<Link to='/add-experience' className='btn btn-light'>
|
||||
<i className='fab fa-black-tie text-primary' /> Add Experience
|
||||
</Link>
|
||||
<Link to='/add-education' className='btn btn-light'>
|
||||
<i className='fas fa-graduation-cap text-primary' /> Add Education
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardActions;
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
import React, { Fragment } from "react";
|
||||
import { deleteEducation } from "../../actions/profile";
|
||||
import formatDate from "../../utils/formatDate";
|
||||
import { EducationType } from "../../types";
|
||||
import { useAppDispatch } from "../../utils/hooks";
|
||||
|
||||
const Education = ({ education }: { education: EducationType[] }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const educations = education.map((edu) => (
|
||||
<tr key={edu._id}>
|
||||
<td>{edu.school}</td>
|
||||
<td className="hide-sm">{edu.degree}</td>
|
||||
<td>
|
||||
{formatDate(edu.from)} - {edu.to ? formatDate(edu.to) : "Now"}
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
onClick={async () => await dispatch(deleteEducation(edu._id))}
|
||||
className="btn btn-danger"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
));
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<h2 className="my-2">Education Credentials</h2>
|
||||
<table className="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>School</th>
|
||||
<th className="hide-sm">Degree</th>
|
||||
<th className="hide-sm">Years</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>{educations}</tbody>
|
||||
</table>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default Education;
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import { deleteExperience } from '../../actions/profile';
|
||||
import { ExperienceType } from '../../types';
|
||||
import formatDate from '../../utils/formatDate';
|
||||
import { useAppDispatch } from '../../utils/hooks';
|
||||
|
||||
const Experience = ({experience}:{experience:ExperienceType[]}) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const experiences = experience.map((exp) => (
|
||||
<tr key={exp._id}>
|
||||
<td>{exp.company}</td>
|
||||
<td className="hide-sm">{exp.title}</td>
|
||||
<td>
|
||||
{formatDate(exp.from)} - {exp.to ? formatDate(exp.to) : 'Now'}
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
onClick={async () => dispatch(deleteExperience(exp._id))}
|
||||
className="btn btn-danger"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
));
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<h2 className="my-2">Experience Credentials</h2>
|
||||
<table className="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Company</th>
|
||||
<th className="hide-sm">Title</th>
|
||||
<th className="hide-sm">Years</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>{experiences}</tbody>
|
||||
</table>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
||||
export default Experience;
|
||||
Executable
+16
@@ -0,0 +1,16 @@
|
||||
import React from "react";
|
||||
import { useAppSelector } from "../../utils/hooks";
|
||||
const Alert = () => {
|
||||
const alerts = useAppSelector((state) => state.alert);
|
||||
return (
|
||||
<div className="alert-wrapper">
|
||||
{alerts.map((alert) => (
|
||||
<div key={alert.id} className={`alert alert-${alert.alertType}`}>
|
||||
{alert.msg}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Alert;
|
||||
Executable
+34
@@ -0,0 +1,34 @@
|
||||
import React from "react";
|
||||
import { Link, Navigate } from "react-router-dom";
|
||||
import { useAppSelector } from "../../utils/hooks";
|
||||
|
||||
const Landing = () => {
|
||||
const isAuthenticated = useAppSelector((state) => state.auth.isAuthenticated);
|
||||
if (isAuthenticated) {
|
||||
return <Navigate to="/dashboard" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="landing">
|
||||
<div className="dark-overlay">
|
||||
<div className="landing-inner">
|
||||
<h1 className="x-large">Developer Connector</h1>
|
||||
<p className="lead">
|
||||
Create a developer profile/portfolio, share posts and get help from
|
||||
other developers
|
||||
</p>
|
||||
<div className="buttons">
|
||||
<Link to="/register" className="btn btn-primary">
|
||||
Sign Up
|
||||
</Link>
|
||||
<Link to="/login" className="btn btn-light">
|
||||
Login
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Landing;
|
||||
Executable
+57
@@ -0,0 +1,57 @@
|
||||
import React, { Fragment } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { logOut } from "../../reducers/auth";
|
||||
import { useAppDispatch, useAppSelector } from "../../utils/hooks";
|
||||
|
||||
const Navbar = () => {
|
||||
const isAuthenticated = useAppSelector((state) => state.auth.isAuthenticated);
|
||||
const dispatch = useAppDispatch();
|
||||
const authLinks = (
|
||||
<ul>
|
||||
<li>
|
||||
<Link to="/profiles">Developers</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="/posts">Posts</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="/dashboard">
|
||||
<i className="fas fa-user" /> <span className="hide-sm">Profile</span>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<a onClick={()=>dispatch(logOut())} href="#!">
|
||||
<i className="fas fa-sign-out-alt" />{' '}
|
||||
<span className="hide-sm">Logout</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
);
|
||||
|
||||
const guestLinks = (
|
||||
<ul>
|
||||
<li>
|
||||
<Link to="/profiles">Developers</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="/register">Register</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="/login">Login</Link>
|
||||
</li>
|
||||
</ul>
|
||||
);
|
||||
|
||||
return (
|
||||
<nav className="navbar bg-dark">
|
||||
<h1>
|
||||
<Link to="/posts">
|
||||
<i className="fas fa-code" /> DevConnector
|
||||
</Link>
|
||||
</h1>
|
||||
<Fragment>{isAuthenticated ? authLinks : guestLinks}</Fragment>
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
||||
export default Navbar;
|
||||
Executable
+14
@@ -0,0 +1,14 @@
|
||||
import React from 'react';
|
||||
|
||||
const NotFound = () => {
|
||||
return (
|
||||
<section className="container">
|
||||
<h1 className="x-large text-primary">
|
||||
<i className="fas fa-exclamation-triangle" /> Page Not Found
|
||||
</h1>
|
||||
<p className="large">Sorry, this page does not exist</p>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotFound;
|
||||
Executable
+14
@@ -0,0 +1,14 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import spinner from './spinner.gif';
|
||||
|
||||
const Spinner = () => (
|
||||
<Fragment>
|
||||
<img
|
||||
src={spinner}
|
||||
style={{ width: '200px', margin: 'auto', display: 'block' }}
|
||||
alt="Loading..."
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
export default Spinner;
|
||||
Executable
+39
@@ -0,0 +1,39 @@
|
||||
import React, { useState } from 'react';
|
||||
import { addComment } from '../../actions/post';
|
||||
import { useAppDispatch } from '../../utils/hooks';
|
||||
|
||||
const CommentForm = ({ postId }: { postId: string }) => {
|
||||
const [text, setText] = useState('');
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
return (
|
||||
<div className='post-form'>
|
||||
<div className='bg-primary p'>
|
||||
<h3>Leave a Comment</h3>
|
||||
</div>
|
||||
<form
|
||||
className='form my-1'
|
||||
onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
await dispatch(addComment(postId, { text }));
|
||||
setText('');
|
||||
}}
|
||||
>
|
||||
<textarea
|
||||
name='text'
|
||||
cols={30}
|
||||
rows={5}
|
||||
placeholder='Comment the post'
|
||||
value={text}
|
||||
onChange={e => setText(e.target.value)}
|
||||
required
|
||||
/>
|
||||
<input type='submit' className='btn btn-dark my-1' value='Submit' />
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export default CommentForm
|
||||
|
||||
Executable
+42
@@ -0,0 +1,42 @@
|
||||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import formatDate from "../../utils/formatDate";
|
||||
import { deleteComment } from "../../actions/post";
|
||||
import { useAppDispatch, useAppSelector } from "../../utils/hooks";
|
||||
import { Comment } from "../../types";
|
||||
interface CommentItemProps {
|
||||
postId: string,
|
||||
comment: Comment
|
||||
}
|
||||
const CommentItem = ({
|
||||
postId,
|
||||
comment: { _id, text, name, avatar, user, date },
|
||||
}: CommentItemProps) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const auth = useAppSelector((state) => state.auth);
|
||||
return (
|
||||
<div className="post bg-white p-1 my-1">
|
||||
<div>
|
||||
<Link to={`/profile/${user}`}>
|
||||
<img className="round-img" src={avatar} alt="" />
|
||||
<h4>{name}</h4>
|
||||
</Link>
|
||||
</div>
|
||||
<div>
|
||||
<p className="my-1">{text}</p>
|
||||
<p className="post-date">Posted on {formatDate(date)}</p>
|
||||
{!auth.loading && auth.user !== null && user === auth.user._id && (
|
||||
<button
|
||||
onClick={async () => await dispatch(deleteComment(postId, _id))}
|
||||
type="button"
|
||||
className="btn btn-danger"
|
||||
>
|
||||
<i className="fas fa-times" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CommentItem;
|
||||
Executable
+39
@@ -0,0 +1,39 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { Link, useParams } from "react-router-dom";
|
||||
import Spinner from "../layout/Spinner";
|
||||
import PostItem from "../posts/PostItem";
|
||||
import CommentForm from "../post/CommentForm";
|
||||
import CommentItem from "../post/CommentItem";
|
||||
import { getPost } from "../../actions/post";
|
||||
import { useAppDispatch, useAppSelector } from "../../utils/hooks";
|
||||
|
||||
const Post = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { post, loading } = useAppSelector((state) => state.post);
|
||||
const { id } = useParams();
|
||||
useEffect(() => {
|
||||
async function fetchData() {
|
||||
if (id !== undefined) await dispatch(getPost(id));
|
||||
}
|
||||
fetchData();
|
||||
}, [dispatch, id]);
|
||||
|
||||
return loading || post === null ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<section className="container">
|
||||
<Link to="/posts" className="btn">
|
||||
Back To Posts
|
||||
</Link>
|
||||
<PostItem post={post} />
|
||||
<CommentForm postId={post._id} />
|
||||
<div className="comments">
|
||||
{post.comments.map((comment) => (
|
||||
<CommentItem key={comment._id} comment={comment} postId={post._id} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Post;
|
||||
Executable
+36
@@ -0,0 +1,36 @@
|
||||
import React, { useState } from "react";
|
||||
import { addPost } from "../../actions/post";
|
||||
import { useAppDispatch } from "../../utils/hooks";
|
||||
|
||||
const PostForm = () => {
|
||||
const [text, setText] = useState("");
|
||||
const dispatch = useAppDispatch();
|
||||
return (
|
||||
<div className="post-form">
|
||||
<div className="bg-primary p">
|
||||
<h3>Say Something...</h3>
|
||||
</div>
|
||||
<form
|
||||
className="form my-1"
|
||||
onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
await dispatch(addPost({ text }));
|
||||
setText("");
|
||||
}}
|
||||
>
|
||||
<textarea
|
||||
name="text"
|
||||
cols={30}
|
||||
rows={5}
|
||||
placeholder="Create a post"
|
||||
value={text}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
required
|
||||
/>
|
||||
<input type="submit" className="btn btn-dark my-1" value="Submit" />
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PostForm;
|
||||
Executable
+58
@@ -0,0 +1,58 @@
|
||||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import formatDate from "../../utils/formatDate";
|
||||
import { addLike, removeLike, deletePost } from "../../actions/post";
|
||||
import { Post } from "../../types";
|
||||
import { useAppDispatch, useAppSelector } from "../../utils/hooks";
|
||||
|
||||
const PostItem = ({
|
||||
post: { _id, text, name, avatar, user, likes, comments, date },
|
||||
}: { post: Post }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const auth = useAppSelector((state) => state.auth);
|
||||
return (
|
||||
<div className="post bg-white p-1 my-1">
|
||||
<div>
|
||||
<Link to={`/profile/${user}`}>
|
||||
<img className="round-img" src={avatar} alt="" />
|
||||
<h4>{name}</h4>
|
||||
</Link>
|
||||
</div>
|
||||
<div>
|
||||
<p className="my-1">{text}</p>
|
||||
<p className="post-date">Posted on {formatDate(date)}</p>
|
||||
<button
|
||||
onClick={async () => await dispatch(addLike(_id))}
|
||||
type="button"
|
||||
className="btn btn-light"
|
||||
>
|
||||
<i className="fas fa-thumbs-up" />{" "}
|
||||
<span>{likes.length > 0 && <span>{likes.length}</span>}</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={async () => await dispatch(removeLike(_id))}
|
||||
type="button"
|
||||
className="btn btn-light"
|
||||
>
|
||||
<i className="fas fa-thumbs-down" />
|
||||
</button>
|
||||
<Link to={`/posts/${_id}`} className="btn btn-primary">
|
||||
Discussion{" "}
|
||||
{comments.length > 0 && (
|
||||
<span className="comment-count">{comments.length}</span>
|
||||
)}
|
||||
</Link>
|
||||
{!auth.loading && auth.user !== null && user === auth.user._id && (
|
||||
<button
|
||||
onClick={async () => await dispatch(deletePost(_id))}
|
||||
type="button"
|
||||
className="btn btn-danger"
|
||||
>
|
||||
<i className="fas fa-times" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default PostItem;
|
||||
Executable
+34
@@ -0,0 +1,34 @@
|
||||
import React, { useEffect } from "react";
|
||||
import PostItem from "./PostItem";
|
||||
import PostForm from "./PostForm";
|
||||
import { getPosts } from "../../actions/post";
|
||||
import { useAppDispatch, useAppSelector } from "../../utils/hooks";
|
||||
|
||||
const Posts = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
useEffect(() => {
|
||||
async function fetchData() {
|
||||
await dispatch(getPosts());
|
||||
}
|
||||
fetchData();
|
||||
}, [dispatch]);
|
||||
const posts = useAppSelector((state) => state.post.posts);
|
||||
|
||||
|
||||
return (
|
||||
<section className="container">
|
||||
<h1 className="large text-primary">Posts</h1>
|
||||
<p className="lead">
|
||||
<i className="fas fa-user" /> Welcome to the community
|
||||
</p>
|
||||
<PostForm />
|
||||
<div className="posts">
|
||||
{posts.map((post) => (
|
||||
<PostItem key={post._id} post={post} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Posts;
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
import React, { useState } from "react";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { addEducation } from "../../actions/profile";
|
||||
import { useAppDispatch } from "../../utils/hooks";
|
||||
|
||||
const AddEducation = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const navigate = useNavigate();
|
||||
const [formData, setFormData] = useState({
|
||||
school: "",
|
||||
degree: "",
|
||||
fieldofstudy: "",
|
||||
from: "",
|
||||
to: "",
|
||||
current: false,
|
||||
description: "",
|
||||
});
|
||||
|
||||
const { school, degree, fieldofstudy, from, to, description, current } =
|
||||
formData;
|
||||
|
||||
const onChange = (event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) =>
|
||||
setFormData({ ...formData, [event.target.name]: event.target.value });
|
||||
|
||||
return (
|
||||
<section className="container">
|
||||
<h1 className="large text-primary">Add Your Education</h1>
|
||||
<p className="lead">
|
||||
<i className="fas fa-code-branch" /> Add any school or bootcamp that you
|
||||
have attended
|
||||
</p>
|
||||
<small>* = required field</small>
|
||||
<form
|
||||
className="form"
|
||||
onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
await dispatch(addEducation(formData)).then(() => navigate("/dashboard"));
|
||||
// i have no idea how this works used to work, i removed the navigate function from the addEducation and it does now
|
||||
}}
|
||||
>
|
||||
<div className="form-group">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="* School or Bootcamp"
|
||||
name="school"
|
||||
value={school}
|
||||
onChange={onChange}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="* Degree or Certificate"
|
||||
name="degree"
|
||||
value={degree}
|
||||
onChange={onChange}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Field of Study"
|
||||
name="fieldofstudy"
|
||||
value={fieldofstudy}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<h4>From Date</h4>
|
||||
<input type="date" name="from" value={from} onChange={onChange} />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<p>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="current"
|
||||
checked={current}
|
||||
//TODO this fuckery here
|
||||
value={current as unknown as string}
|
||||
onChange={() => setFormData({ ...formData, current: !current })}
|
||||
/>{" "}
|
||||
Current School
|
||||
</p>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<h4>To Date</h4>
|
||||
<input
|
||||
type="date"
|
||||
name="to"
|
||||
value={to}
|
||||
onChange={onChange}
|
||||
disabled={current}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<textarea
|
||||
name="description"
|
||||
cols={30}
|
||||
rows={5}
|
||||
placeholder="Program Description"
|
||||
value={description}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
<input type="submit" className="btn btn-primary my-1" />
|
||||
<Link className="btn btn-light my-1" to="/dashboard">
|
||||
Go Back
|
||||
</Link>
|
||||
</form>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export default AddEducation;
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { addExperience } from '../../actions/profile';
|
||||
import { useAppDispatch } from '../../utils/hooks';
|
||||
|
||||
const AddExperience = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const navigate = useNavigate();
|
||||
const [formData, setFormData] = useState({
|
||||
company: '',
|
||||
title: '',
|
||||
location: '',
|
||||
from: '',
|
||||
to: '',
|
||||
current: false,
|
||||
description: ''
|
||||
});
|
||||
|
||||
const { company, title, location, from, to, current, description } = formData;
|
||||
|
||||
const onChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) =>
|
||||
setFormData({ ...formData, [e.target.name]: e.target.value });
|
||||
|
||||
return (
|
||||
<section className="container">
|
||||
<h1 className="large text-primary">Add An Experience</h1>
|
||||
<p className="lead">
|
||||
<i className="fas fa-code-branch" /> Add any developer/programming
|
||||
positions that you have had in the past
|
||||
</p>
|
||||
<small>* = required field</small>
|
||||
<form
|
||||
className="form"
|
||||
onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
await dispatch(addExperience(formData)).then(() => navigate('/dashboard'));
|
||||
}}
|
||||
>
|
||||
<div className="form-group">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="* Job Title"
|
||||
name="title"
|
||||
value={title}
|
||||
onChange={onChange}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="* Company"
|
||||
name="company"
|
||||
value={company}
|
||||
onChange={onChange}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Location"
|
||||
name="location"
|
||||
value={location}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<h4>From Date</h4>
|
||||
<input type="date" name="from" value={from} onChange={onChange} />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<p>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="current"
|
||||
checked={current}
|
||||
value={current as unknown as string}
|
||||
onChange={() => {
|
||||
setFormData({ ...formData, current: !current });
|
||||
}}
|
||||
/>{' '}
|
||||
Current Job
|
||||
</p>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<h4>To Date</h4>
|
||||
<input
|
||||
type="date"
|
||||
name="to"
|
||||
value={to}
|
||||
onChange={onChange}
|
||||
disabled={current}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<textarea
|
||||
name="description"
|
||||
cols={30}
|
||||
rows={5}
|
||||
placeholder="Job Description"
|
||||
value={description}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
<input type="submit" className="btn btn-primary my-1" />
|
||||
<Link className="btn btn-light my-1" to="/dashboard">
|
||||
Go Back
|
||||
</Link>
|
||||
</form>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddExperience;
|
||||
+269
@@ -0,0 +1,269 @@
|
||||
import React, { Fragment, useState, useEffect } from "react";
|
||||
import { Link, useMatch, useNavigate } from "react-router-dom";
|
||||
import { createProfile, getCurrentProfile } from "../../actions/profile";
|
||||
import { useAppDispatch, useAppSelector } from "../../utils/hooks";
|
||||
|
||||
const initialState = {
|
||||
company: "",
|
||||
website: "",
|
||||
location: "",
|
||||
status: "",
|
||||
skills: "",
|
||||
githubusername: "",
|
||||
bio: "",
|
||||
twitter: "",
|
||||
facebook: "",
|
||||
linkedin: "",
|
||||
youtube: "",
|
||||
instagram: "",
|
||||
};
|
||||
|
||||
const ProfileForm = () => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const { profile, loading }: { profile: any, loading: boolean } = useAppSelector((state) => state.profile);
|
||||
//TODO
|
||||
// issue with for in
|
||||
|
||||
const [formData, setFormData] = useState(initialState);
|
||||
|
||||
const creatingProfile = useMatch("/create-profile");
|
||||
|
||||
const [displaySocialInputs, toggleSocialInputs] = useState(false);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
// if there is no profile, attempt to fetch one
|
||||
async function fetchData() {
|
||||
await dispatch(getCurrentProfile())
|
||||
}
|
||||
if (!profile) fetchData();
|
||||
|
||||
// if we finished loading and we do have a profile
|
||||
// then build our profileData
|
||||
if (!loading && profile) {
|
||||
const profileData: any = { ...initialState };
|
||||
// cant figure out how to type key to be keyof profile so they are any now
|
||||
for (const key in profile) {
|
||||
if (key in profileData) profileData[key] = profile[key];
|
||||
}
|
||||
for (const key in profile.social) {
|
||||
if (key in profileData) profileData[key] = profile.social[key];
|
||||
}
|
||||
// the skills may be an array from our API response
|
||||
if (Array.isArray(profileData.skills))
|
||||
profileData.skills = profileData.skills.join(", ");
|
||||
// set local state with the profileData
|
||||
setFormData(profileData);
|
||||
}
|
||||
}, [loading, dispatch, profile]);
|
||||
|
||||
const {
|
||||
company,
|
||||
website,
|
||||
location,
|
||||
status,
|
||||
skills,
|
||||
githubusername,
|
||||
bio,
|
||||
twitter,
|
||||
facebook,
|
||||
linkedin,
|
||||
youtube,
|
||||
instagram,
|
||||
} = formData;
|
||||
|
||||
const onChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) =>
|
||||
setFormData({ ...formData, [e.target.name]: e.target.value });
|
||||
|
||||
const onSubmit = async (e: React.SyntheticEvent) => {
|
||||
const editing = profile ? true : false;
|
||||
e.preventDefault();
|
||||
await dispatch(createProfile(formData, editing)).then(() => {
|
||||
console.log(editing)
|
||||
if (!editing) navigate("/dashboard");
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="container">
|
||||
<h1 className="large text-primary">
|
||||
{creatingProfile ? "Create Your Profile" : "Edit Your Profile"}
|
||||
</h1>
|
||||
<p className="lead">
|
||||
<i className="fas fa-user" />
|
||||
{creatingProfile
|
||||
? ` Let's get some information to make your`
|
||||
: " Add some changes to your profile"}
|
||||
</p>
|
||||
<small>* = required field</small>
|
||||
<form className="form" onSubmit={onSubmit}>
|
||||
<div className="form-group">
|
||||
<select name="status" value={status} onChange={onChange}>
|
||||
<option>* Select Professional Status</option>
|
||||
<option value="Developer">Developer</option>
|
||||
<option value="Junior Developer">Junior Developer</option>
|
||||
<option value="Senior Developer">Senior Developer</option>
|
||||
<option value="Manager">Manager</option>
|
||||
<option value="Student or Learning">Student or Learning</option>
|
||||
<option value="Instructor">Instructor or Teacher</option>
|
||||
<option value="Intern">Intern</option>
|
||||
<option value="Other">Other</option>
|
||||
</select>
|
||||
<small className="form-text">
|
||||
Give us an idea of where you are at in your career
|
||||
</small>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Company"
|
||||
name="company"
|
||||
value={company}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<small className="form-text">
|
||||
Could be your own company or one you work for
|
||||
</small>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Website"
|
||||
name="website"
|
||||
value={website}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<small className="form-text">
|
||||
Could be your own or a company website
|
||||
</small>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Location"
|
||||
name="location"
|
||||
value={location}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<small className="form-text">
|
||||
City & state suggested (eg. Boston, MA)
|
||||
</small>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="* Skills"
|
||||
name="skills"
|
||||
value={skills}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<small className="form-text">
|
||||
Please use comma separated values (eg. HTML,CSS,JavaScript,PHP)
|
||||
</small>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Github Username"
|
||||
name="githubusername"
|
||||
value={githubusername}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<small className="form-text">
|
||||
If you want your latest repos and a Github link, include your
|
||||
username
|
||||
</small>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<textarea
|
||||
placeholder="A short bio of yourself"
|
||||
name="bio"
|
||||
value={bio}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<small className="form-text">Tell us a little about yourself</small>
|
||||
</div>
|
||||
|
||||
<div className="my-2">
|
||||
<button
|
||||
onClick={() => toggleSocialInputs(!displaySocialInputs)}
|
||||
type="button"
|
||||
className="btn btn-light"
|
||||
>
|
||||
Add Social Network Links
|
||||
</button>
|
||||
<span>Optional</span>
|
||||
</div>
|
||||
|
||||
{displaySocialInputs && (
|
||||
<Fragment>
|
||||
<div className="form-group social-input">
|
||||
<i className="fab fa-twitter fa-2x" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Twitter URL"
|
||||
name="twitter"
|
||||
value={twitter}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group social-input">
|
||||
<i className="fab fa-facebook fa-2x" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Facebook URL"
|
||||
name="facebook"
|
||||
value={facebook}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group social-input">
|
||||
<i className="fab fa-youtube fa-2x" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="YouTube URL"
|
||||
name="youtube"
|
||||
value={youtube}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group social-input">
|
||||
<i className="fab fa-linkedin fa-2x" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Linkedin URL"
|
||||
name="linkedin"
|
||||
value={linkedin}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group social-input">
|
||||
<i className="fab fa-instagram fa-2x" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Instagram URL"
|
||||
name="instagram"
|
||||
value={instagram}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
</Fragment>
|
||||
)}
|
||||
|
||||
<input type="submit" className="btn btn-primary my-1" />
|
||||
<Link className="btn btn-light my-1" to="/dashboard">
|
||||
Go Back
|
||||
</Link>
|
||||
</form>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfileForm;
|
||||
Executable
+89
@@ -0,0 +1,89 @@
|
||||
import React, { Fragment, useEffect } from "react";
|
||||
import { Link, useParams } from "react-router-dom";
|
||||
import Spinner from "../layout/Spinner";
|
||||
import ProfileTop from "./ProfileTop";
|
||||
import ProfileAbout from "./ProfileAbout";
|
||||
import ProfileExperience from "./ProfileExperience";
|
||||
import ProfileEducation from "./ProfileEducation";
|
||||
import ProfileGithub from "./ProfileGithub";
|
||||
import { getProfileById } from "../../actions/profile";
|
||||
import { useAppDispatch, useAppSelector } from "../../utils/hooks";
|
||||
import { EducationType, ExperienceType } from "../../types";
|
||||
|
||||
const Profile = () => {
|
||||
const profile = useAppSelector((state) => state.profile.profile);
|
||||
const auth = useAppSelector((state) => state.auth);
|
||||
const dispatch = useAppDispatch();
|
||||
const { id } = useParams();
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchData() {
|
||||
if (typeof id === 'string')
|
||||
await dispatch(getProfileById(id));
|
||||
}
|
||||
fetchData();
|
||||
}, [dispatch, id]);
|
||||
|
||||
return (
|
||||
<section className="container">
|
||||
{profile === null ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<Fragment>
|
||||
<Link to="/profiles" className="btn btn-light">
|
||||
Back To Profiles
|
||||
</Link>
|
||||
{auth.isAuthenticated &&
|
||||
auth.loading === false &&
|
||||
auth.user !== null &&
|
||||
auth.user._id === profile.user._id && (
|
||||
<Link to="/edit-profile" className="btn btn-dark">
|
||||
Edit Profile
|
||||
</Link>
|
||||
)}
|
||||
<div className="profile-grid my-1">
|
||||
<ProfileTop profile={profile} />
|
||||
<ProfileAbout profile={profile} />
|
||||
<div className="profile-exp bg-white p-2">
|
||||
<h2 className="text-primary">Experience</h2>
|
||||
{profile.experience.length > 0 ? (
|
||||
<Fragment>
|
||||
{profile.experience.map((experience:ExperienceType) => (
|
||||
<ProfileExperience
|
||||
key={experience._id}
|
||||
experience={experience}
|
||||
/>
|
||||
))}
|
||||
</Fragment>
|
||||
) : (
|
||||
<h4>No experience credentials</h4>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="profile-edu bg-white p-2">
|
||||
<h2 className="text-primary">Education</h2>
|
||||
{profile.education.length > 0 ? (
|
||||
<Fragment>
|
||||
{profile.education.map((education:EducationType) => (
|
||||
<ProfileEducation
|
||||
key={education._id}
|
||||
education={education}
|
||||
/>
|
||||
))}
|
||||
</Fragment>
|
||||
) : (
|
||||
<h4>No education credentials</h4>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{profile.githubusername && (
|
||||
<ProfileGithub username={profile.githubusername} />
|
||||
)}
|
||||
</div>
|
||||
</Fragment>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Profile;
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import { ProfileType } from '../../types';
|
||||
|
||||
const ProfileAbout = ({
|
||||
profile: {
|
||||
bio,
|
||||
skills,
|
||||
user: { name }
|
||||
}
|
||||
}:{profile:ProfileType}) => (
|
||||
<div className='profile-about bg-light p-2'>
|
||||
{bio && (
|
||||
<Fragment>
|
||||
<h2 className='text-primary'>{name.trim().split(' ')[0]}s Bio</h2>
|
||||
<p>{bio}</p>
|
||||
<div className='line' />
|
||||
</Fragment>
|
||||
)}
|
||||
<h2 className='text-primary'>Skill Set</h2>
|
||||
<div className='skills'>
|
||||
{skills.map((skill, index) => (
|
||||
<div key={index} className='p-1'>
|
||||
<i className='fas fa-check' /> {skill}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
export default ProfileAbout;
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import { EducationType } from '../../types';
|
||||
import formatDate from '../../utils/formatDate';
|
||||
|
||||
const ProfileEducation = ({
|
||||
education: { school, degree, fieldofstudy, current, to, from, description }
|
||||
}: { education: EducationType }) => (
|
||||
<div>
|
||||
<h3 className="text-dark">{school}</h3>
|
||||
<p>
|
||||
{formatDate(from)} - {to ? formatDate(to) : 'Now'}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Degree: </strong> {degree}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Field Of Study: </strong> {fieldofstudy}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Description: </strong> {description}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
|
||||
export default ProfileEducation;
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
import { ExperienceType } from '../../types';
|
||||
import formatDate from '../../utils/formatDate';
|
||||
|
||||
const ProfileExperience = ({
|
||||
experience: { company, title, location, current, to, from, description }
|
||||
}: { experience: ExperienceType }) => (
|
||||
<div>
|
||||
<h3 className="text-dark">{company}</h3>
|
||||
<p>
|
||||
{formatDate(from)} - {to ? formatDate(to) : 'Now'}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Position: </strong> {title}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Location: </strong> {location}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Description: </strong> {description}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
export default ProfileExperience;
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { getGithubRepos } from "../../actions/profile";
|
||||
import { useAppDispatch, useAppSelector } from "../../utils/hooks";
|
||||
|
||||
const ProfileGithub = ({ username }: { username: string }) => {
|
||||
const repos = useAppSelector((state) => state.profile.repos);
|
||||
const dispatch = useAppDispatch();
|
||||
useEffect(() => {
|
||||
async function fetchData() {
|
||||
await dispatch(getGithubRepos(username));
|
||||
}
|
||||
fetchData();
|
||||
}, [dispatch, username]);
|
||||
|
||||
return (
|
||||
<div className="profile-github">
|
||||
<h2 className="text-primary my-1">Github Repos</h2>
|
||||
{repos.map((repo) => (
|
||||
<div key={repo.id} className="repo bg-white p-1 my-1">
|
||||
<div>
|
||||
<h4>
|
||||
<a href={repo.html_url} target="_blank" rel="noopener noreferrer">
|
||||
{repo.name}
|
||||
</a>
|
||||
</h4>
|
||||
<p>{repo.description}</p>
|
||||
</div>
|
||||
<div>
|
||||
<ul>
|
||||
<li className="badge badge-primary">
|
||||
Stars: {repo.stargazers_count}
|
||||
</li>
|
||||
<li className="badge badge-dark">
|
||||
Watchers: {repo.watchers_count}
|
||||
</li>
|
||||
<li className="badge badge-light">Forks: {repo.forks_count}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfileGithub;
|
||||
Executable
+47
@@ -0,0 +1,47 @@
|
||||
import React from "react";
|
||||
import { ProfileType } from "../../types";
|
||||
|
||||
const ProfileTop = ({
|
||||
profile: {
|
||||
status,
|
||||
company,
|
||||
location,
|
||||
website,
|
||||
social,
|
||||
user: { name, avatar },
|
||||
},
|
||||
}: { profile: ProfileType }) => {
|
||||
return (
|
||||
<div className="profile-top bg-primary p-2">
|
||||
<img className="round-img my-1" src={avatar} alt="" />
|
||||
<h1 className="large">{name}</h1>
|
||||
<p className="lead">
|
||||
{status} {company ? <span> at {company}</span> : null}
|
||||
</p>
|
||||
<p>{location ? <span>{location}</span> : null}</p>
|
||||
<div className="icons my-1">
|
||||
{website ? (
|
||||
<a href={website} target="_blank" rel="noopener noreferrer">
|
||||
<i className="fas fa-globe fa-2x" />
|
||||
</a>
|
||||
) : null}
|
||||
{social
|
||||
? Object.entries(social)
|
||||
.filter(([_, value]) => value)
|
||||
.map(([key, value]) => (
|
||||
<a
|
||||
key={key}
|
||||
href={value}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<i className={`fab fa-${key} fa-2x`}></i>
|
||||
</a>
|
||||
))
|
||||
: null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfileTop;
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { ProfileType } from '../../types';
|
||||
|
||||
const ProfileItem = ({
|
||||
profile: {
|
||||
user: { _id, name, avatar },
|
||||
status,
|
||||
company,
|
||||
location,
|
||||
skills
|
||||
}
|
||||
}: { profile: ProfileType }) => {
|
||||
return (
|
||||
<div className='profile bg-light'>
|
||||
<img src={avatar} alt='' className='round-img' />
|
||||
<div>
|
||||
<h2>{name}</h2>
|
||||
<p>
|
||||
{status} {company && <span> at {company}</span>}
|
||||
</p>
|
||||
<p className='my-1'>{location && <span>{location}</span>}</p>
|
||||
<Link to={`/profile/${_id}`} className='btn btn-primary'>
|
||||
View Profile
|
||||
</Link>
|
||||
</div>
|
||||
<ul>
|
||||
{skills.slice(0, 4).map((skill, index) => (
|
||||
<li key={index} className='text-primary'>
|
||||
<i className='fas fa-check' /> {skill}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfileItem;
|
||||
|
||||
Executable
+44
@@ -0,0 +1,44 @@
|
||||
import React, { Fragment, useEffect } from "react";
|
||||
import Spinner from "../layout/Spinner";
|
||||
import ProfileItem from "./ProfileItem";
|
||||
import { getProfiles } from "../../actions/profile";
|
||||
import { useAppDispatch, useAppSelector } from "../../utils/hooks";
|
||||
|
||||
const Profiles = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchData() {
|
||||
await dispatch(getProfiles());
|
||||
}
|
||||
fetchData();
|
||||
}, [dispatch]);
|
||||
|
||||
const { profiles, loading } = useAppSelector((state) => state.profile);
|
||||
return (
|
||||
<section className="container">
|
||||
{loading ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<Fragment>
|
||||
<h1 className="large text-primary">Developers</h1>
|
||||
<p className="lead">
|
||||
<i className="fab fa-connectdevelop" /> Browse and connect with
|
||||
developers
|
||||
</p>
|
||||
<div className="profiles">
|
||||
{profiles.length > 0 ? (
|
||||
profiles.map((profile) => (
|
||||
<ProfileItem key={profile._id} profile={profile} />
|
||||
))
|
||||
) : (
|
||||
<h4>No profiles found...</h4>
|
||||
)}
|
||||
</div>
|
||||
</Fragment>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Profiles;
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import React from "react";
|
||||
import { Navigate } from "react-router-dom";
|
||||
import Spinner from "../layout/Spinner";
|
||||
import { useAppSelector } from "../../utils/hooks";
|
||||
|
||||
const PrivateRoute = ({ component: Component }: { component: () => JSX.Element }) => {
|
||||
const { isAuthenticated, loading } = useAppSelector((state) => state.auth);
|
||||
if (loading) return <Spinner />;
|
||||
if (isAuthenticated) return <Component />;
|
||||
|
||||
return <Navigate to="/login" />;
|
||||
};
|
||||
|
||||
export default PrivateRoute;
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
declare module "*.gif" {
|
||||
const path: string;
|
||||
export default path
|
||||
}
|
||||
// bonkers have no idea how this works
|
||||
@@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
|
||||
const root = ReactDOM.createRoot(
|
||||
document.getElementById('root') as HTMLElement
|
||||
);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
Executable
+24
@@ -0,0 +1,24 @@
|
||||
import { createSlice } from "@reduxjs/toolkit";
|
||||
import type { Alert } from "../types";
|
||||
import { PayloadAction } from "@reduxjs/toolkit";
|
||||
|
||||
type AlertState = Alert[]
|
||||
|
||||
const initialState: AlertState = [];
|
||||
|
||||
const alertSlice = createSlice({
|
||||
name: "alert",
|
||||
initialState,
|
||||
reducers: {
|
||||
setAlert(state, action: PayloadAction<Alert>) {
|
||||
return [...state, action.payload];
|
||||
},
|
||||
removeAlert(state, action: PayloadAction<string>) {
|
||||
return state.filter((alert) => alert.id !== action.payload);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { setAlert, removeAlert } = alertSlice.actions;
|
||||
|
||||
export default alertSlice.reducer;
|
||||
Executable
+85
@@ -0,0 +1,85 @@
|
||||
import { createSlice } from "@reduxjs/toolkit";
|
||||
import { PayloadAction } from "@reduxjs/toolkit";
|
||||
import type { User } from '../types'
|
||||
|
||||
interface authState {
|
||||
token: string | null
|
||||
isAuthenticated: boolean | null
|
||||
loading: boolean
|
||||
user: User | null
|
||||
}
|
||||
|
||||
const initialState: authState = {
|
||||
token: localStorage.getItem("token"),
|
||||
isAuthenticated: null,
|
||||
loading: true,
|
||||
user: null,
|
||||
};
|
||||
|
||||
const authSlice = createSlice({
|
||||
name: "auth",
|
||||
initialState,
|
||||
reducers: {
|
||||
userLoaded(state, action: PayloadAction<User>) {
|
||||
return {
|
||||
...state,
|
||||
isAuthenticated: true,
|
||||
loading: false,
|
||||
user: action.payload,
|
||||
};
|
||||
},
|
||||
registerSuccess(state, action) {
|
||||
return {
|
||||
...state,
|
||||
...action.payload,
|
||||
isAuthenticated: true,
|
||||
loading: false,
|
||||
};
|
||||
},
|
||||
loginSucces(state, action) {
|
||||
return {
|
||||
...state,
|
||||
...action.payload,
|
||||
isAuthenticated: true,
|
||||
loading: false,
|
||||
};
|
||||
},
|
||||
accountDeleted(state) {
|
||||
return {
|
||||
...state,
|
||||
token: null,
|
||||
isAuthenticated: false,
|
||||
loading: false,
|
||||
user: null,
|
||||
};
|
||||
},
|
||||
authError(state) {
|
||||
return {
|
||||
...state,
|
||||
token: null,
|
||||
isAuthenticated: false,
|
||||
loading: false,
|
||||
user: null,
|
||||
};
|
||||
},
|
||||
logOut(state) {
|
||||
return {
|
||||
...state,
|
||||
token: null,
|
||||
isAuthenticated: false,
|
||||
loading: false,
|
||||
user: null,
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
export const {
|
||||
logOut,
|
||||
userLoaded,
|
||||
loginSucces,
|
||||
registerSuccess,
|
||||
accountDeleted,
|
||||
authError,
|
||||
} = authSlice.actions;
|
||||
|
||||
export default authSlice.reducer;
|
||||
Executable
+109
@@ -0,0 +1,109 @@
|
||||
import { createSlice } from "@reduxjs/toolkit";
|
||||
import { PayloadAction } from "@reduxjs/toolkit";
|
||||
import type { Post, Comment } from "../types";
|
||||
|
||||
interface postState {
|
||||
posts: Post[]
|
||||
post: Post | null
|
||||
loading: boolean
|
||||
error: {}
|
||||
//Todo erros
|
||||
}
|
||||
const initialState: postState = {
|
||||
posts: [],
|
||||
post: null,
|
||||
loading: true,
|
||||
error: {},
|
||||
};
|
||||
|
||||
const postSlice = createSlice({
|
||||
name: "post",
|
||||
initialState,
|
||||
reducers: {
|
||||
getPostsAction(state, action) {
|
||||
return {
|
||||
...state,
|
||||
posts: action.payload,
|
||||
loading: false,
|
||||
};
|
||||
},
|
||||
getPostAction(state, action) {
|
||||
return {
|
||||
...state,
|
||||
post: action.payload,
|
||||
loading: false,
|
||||
};
|
||||
},
|
||||
addPostAction(state, action) {
|
||||
return {
|
||||
...state,
|
||||
posts: [action.payload, ...state.posts],
|
||||
loading: false,
|
||||
};
|
||||
},
|
||||
deletePostAction(state, action) {
|
||||
return {
|
||||
...state,
|
||||
posts: state.posts.filter((post) => post._id !== action.payload),
|
||||
loading: false,
|
||||
};
|
||||
},
|
||||
postError(state, action) {
|
||||
return {
|
||||
...state,
|
||||
error: action.payload,
|
||||
loading: false,
|
||||
};
|
||||
},
|
||||
|
||||
updateLikes(state, action) {
|
||||
return {
|
||||
...state,
|
||||
posts: state.posts.map((post) =>
|
||||
post._id === action.payload.id
|
||||
? { ...post, likes: action.payload.likes }
|
||||
: post
|
||||
),
|
||||
loading: false,
|
||||
};
|
||||
},
|
||||
|
||||
addCommentAction(state, action: PayloadAction<Comment[]>) {
|
||||
if ("post" in state && state.post !== null) {
|
||||
return {
|
||||
...state,
|
||||
post: { ...state.post, comments: action.payload },
|
||||
loading: false,
|
||||
}
|
||||
}
|
||||
throw new Error('post missing from state')
|
||||
},
|
||||
removeComment(state, action: PayloadAction<String>) {
|
||||
if ("post" in state && state.post !== null) {
|
||||
return {
|
||||
...state,
|
||||
post: {
|
||||
...state.post,
|
||||
comments: state.post.comments.filter(
|
||||
(comment) => comment._id !== action.payload
|
||||
),
|
||||
},
|
||||
loading: false,
|
||||
};
|
||||
}
|
||||
throw new Error('post missing from state')
|
||||
},
|
||||
},
|
||||
});
|
||||
export const {
|
||||
removeComment,
|
||||
addCommentAction,
|
||||
updateLikes,
|
||||
postError,
|
||||
deletePostAction,
|
||||
getPostAction,
|
||||
getPostsAction,
|
||||
addPostAction,
|
||||
} = postSlice.actions;
|
||||
|
||||
export default postSlice.reducer;
|
||||
Executable
+84
@@ -0,0 +1,84 @@
|
||||
import { createSlice } from "@reduxjs/toolkit";
|
||||
import { Repo, ProfileType } from "../types";
|
||||
|
||||
type ProfileState = {
|
||||
profile: ProfileType | null
|
||||
profiles: ProfileType[]
|
||||
repos: Repo[]
|
||||
loading: boolean
|
||||
error: {}
|
||||
}
|
||||
|
||||
const initialState: ProfileState = {
|
||||
profile: null,
|
||||
profiles: [],
|
||||
repos: [],
|
||||
loading: true,
|
||||
error: {},
|
||||
};
|
||||
const profileSlice = createSlice({
|
||||
name: "profile",
|
||||
initialState,
|
||||
reducers: {
|
||||
getProfile(state, action) {
|
||||
return {
|
||||
...state,
|
||||
profile: action.payload,
|
||||
loading: false,
|
||||
};
|
||||
},
|
||||
updateProfile(state, action) {
|
||||
return {
|
||||
...state,
|
||||
profile: action.payload,
|
||||
loading: false,
|
||||
};
|
||||
},
|
||||
getProfilesType(state, action) {
|
||||
return {
|
||||
...state,
|
||||
profiles: action.payload,
|
||||
loading: false,
|
||||
};
|
||||
},
|
||||
profileError(state, action) {
|
||||
return {
|
||||
...state,
|
||||
error: action.payload,
|
||||
loading: false,
|
||||
profile: null,
|
||||
};
|
||||
},
|
||||
clearProfile(state) {
|
||||
return {
|
||||
...state,
|
||||
profile: null,
|
||||
repos: [],
|
||||
};
|
||||
},
|
||||
getRepos(state, action) {
|
||||
return {
|
||||
...state,
|
||||
repos: action.payload,
|
||||
loading: false,
|
||||
};
|
||||
},
|
||||
noRepos(state) {
|
||||
return {
|
||||
...state,
|
||||
repos: [],
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
export const {
|
||||
noRepos,
|
||||
getRepos,
|
||||
clearProfile,
|
||||
profileError,
|
||||
getProfilesType,
|
||||
updateProfile,
|
||||
getProfile,
|
||||
} = profileSlice.actions;
|
||||
|
||||
export default profileSlice.reducer;
|
||||
Executable
+38
@@ -0,0 +1,38 @@
|
||||
import setAuthToken from './utils/setAuthToken';
|
||||
import { configureStore } from '@reduxjs/toolkit';
|
||||
|
||||
import alertReducer from './reducers/alert';
|
||||
import authReducer from './reducers/auth';
|
||||
import profileReducer from './reducers/profile'
|
||||
import postReducer from './reducers/post'
|
||||
|
||||
|
||||
const store = configureStore({
|
||||
reducer: {
|
||||
alert: alertReducer,
|
||||
auth: authReducer,
|
||||
profile: profileReducer,
|
||||
post: postReducer
|
||||
}
|
||||
})
|
||||
|
||||
let currentState = store.getState();
|
||||
|
||||
store.subscribe(() => {
|
||||
// keep track of the previous and current state to compare changes
|
||||
let previousState = currentState;
|
||||
currentState = store.getState();
|
||||
// if the token changes set the value in localStorage and axios headers
|
||||
if (previousState.auth.token !== currentState.auth.token) {
|
||||
const token = currentState.auth.token;
|
||||
if (typeof token === 'string') setAuthToken(token);
|
||||
else throw new Error("token not string")
|
||||
}
|
||||
});
|
||||
// Infer the `RootState` and `AppDispatch` types from the store itself
|
||||
export type RootState = ReturnType<typeof store.getState>
|
||||
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
|
||||
export type AppDispatch = typeof store.dispatch
|
||||
|
||||
export default store;
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
|
||||
import { AnyAction, ThunkAction } from "@reduxjs/toolkit"
|
||||
import { RootState } from "./store"
|
||||
|
||||
export type AppThunk<ReturnType = void> = ThunkAction<ReturnType, RootState, unknown, AnyAction>
|
||||
|
||||
export type FormData = { name: string, email: string, password: string }
|
||||
|
||||
export type Repo = {
|
||||
id: string
|
||||
html_url: string
|
||||
name: string
|
||||
description: string
|
||||
stargazers_count: number
|
||||
watchers_count: number
|
||||
forks_count: number
|
||||
}
|
||||
export type Post = {
|
||||
user: string,
|
||||
text: string
|
||||
name: string,
|
||||
avatar: string,
|
||||
likes: User[]
|
||||
comments: Comment[]
|
||||
date: string
|
||||
_id: string
|
||||
}
|
||||
export type Comment = {
|
||||
user: string
|
||||
text: string
|
||||
name: string,
|
||||
avatar: string,
|
||||
date: string
|
||||
_id: string
|
||||
}
|
||||
export type User = {
|
||||
name: string,
|
||||
email: string,
|
||||
password: string,
|
||||
avatar: string,
|
||||
data: string
|
||||
_id: string
|
||||
}
|
||||
|
||||
export type ProfileType = {
|
||||
user: User
|
||||
company: string,
|
||||
website: string
|
||||
location: string
|
||||
status: string
|
||||
skills: string[]
|
||||
bio: string
|
||||
githubusername: string
|
||||
experience: ExperienceType[]
|
||||
date: string
|
||||
education: EducationType[]
|
||||
social: Social
|
||||
_id: string
|
||||
|
||||
}
|
||||
export type ExperienceType = {
|
||||
title: string
|
||||
company: string
|
||||
location: string
|
||||
from: string
|
||||
to: string
|
||||
current: boolean
|
||||
description: string
|
||||
_id: string
|
||||
}
|
||||
export type EducationType = {
|
||||
school: string
|
||||
degree: string
|
||||
fieldofstudy: string
|
||||
from: string
|
||||
to: string
|
||||
current: boolean
|
||||
description: string
|
||||
_id: string
|
||||
}
|
||||
export type Social = {
|
||||
youtube: string
|
||||
twitter: string
|
||||
facebook: string
|
||||
linkedin: string
|
||||
instagram: string
|
||||
_id: string
|
||||
}
|
||||
export type Alert = {
|
||||
msg: string
|
||||
alertType: string
|
||||
id: string
|
||||
}
|
||||
Executable
+20
@@ -0,0 +1,20 @@
|
||||
import axios from 'axios';
|
||||
import store from '../store';
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: '/api',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
api.interceptors.response.use(
|
||||
(res) => res,
|
||||
(err) => {
|
||||
if (err.response.status === 401) {
|
||||
store.dispatch({ type: 'auth/logOut' });
|
||||
}
|
||||
return Promise.reject(err);
|
||||
}
|
||||
);
|
||||
|
||||
export default api;
|
||||
Executable
+5
@@ -0,0 +1,5 @@
|
||||
function formatDate(date: string) {
|
||||
return new Intl.DateTimeFormat().format(new Date(date));
|
||||
}
|
||||
|
||||
export default formatDate;
|
||||
@@ -0,0 +1,8 @@
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import type { TypedUseSelectorHook } from 'react-redux'
|
||||
import type { RootState, AppDispatch } from '../store'
|
||||
|
||||
// Use throughout your app instead of plain `useDispatch` and `useSelector`
|
||||
export const useAppDispatch: () => AppDispatch = useDispatch
|
||||
export const useAppDispatch2 = () => useDispatch<AppDispatch>
|
||||
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
|
||||
Executable
+15
@@ -0,0 +1,15 @@
|
||||
import api from './api';
|
||||
|
||||
// store our JWT in LS and set axios headers if we do have a token
|
||||
|
||||
const setAuthToken = (token: string) => {
|
||||
if (token) {
|
||||
api.defaults.headers.common['x-auth-token'] = token;
|
||||
localStorage.setItem('token', token);
|
||||
} else {
|
||||
delete api.defaults.headers.common['x-auth-token'];
|
||||
localStorage.removeItem('token');
|
||||
}
|
||||
};
|
||||
|
||||
export default setAuthToken;
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
}
|
||||
Executable
+20
@@ -0,0 +1,20 @@
|
||||
const mongoose = require('mongoose');
|
||||
const config = require('config');
|
||||
const db = config.get('mongoURI');
|
||||
|
||||
const connectDB = async () => {
|
||||
try {
|
||||
await mongoose.connect(db, {
|
||||
useNewUrlParser: true,
|
||||
useUnifiedTopology: true
|
||||
});
|
||||
|
||||
console.log('MongoDB Connected...');
|
||||
} catch (err) {
|
||||
console.error(err.message);
|
||||
// Exit process with failure
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = connectDB;
|
||||
Executable
+28
@@ -0,0 +1,28 @@
|
||||
const config = require('config');
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
module.exports = function (req, res, next) {
|
||||
|
||||
// Get token from header
|
||||
const token = req.header('x-auth-token');
|
||||
// Check if not token
|
||||
if (!token) {
|
||||
return res.status(401).json({ msg: 'No token, authorization denied' });
|
||||
}
|
||||
|
||||
// Verify token
|
||||
try {
|
||||
jwt.verify(token, config.get('jwtSecret'), (error, decoded) => {
|
||||
if (error) {
|
||||
return res.status(401).json({ msg: 'Token is not valid' });
|
||||
} else {
|
||||
req.user = decoded.user;
|
||||
next();
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('something wrong with auth middleware');
|
||||
res.status(500).json({ msg: 'Server Error' });
|
||||
}
|
||||
|
||||
};
|
||||
Executable
+9
@@ -0,0 +1,9 @@
|
||||
const mongoose = require('mongoose');
|
||||
// middleware to check for a valid object id
|
||||
const checkObjectId = (idToCheck) => (req, res, next) => {
|
||||
if (!mongoose.Types.ObjectId.isValid(req.params[idToCheck]))
|
||||
return res.status(400).json({ msg: 'Invalid ID' });
|
||||
next();
|
||||
};
|
||||
|
||||
module.exports = checkObjectId;
|
||||
Executable
+52
@@ -0,0 +1,52 @@
|
||||
const mongoose = require("mongoose");
|
||||
const Schema = mongoose.Schema;
|
||||
|
||||
const PostSchema = new Schema({
|
||||
user: {
|
||||
type: Schema.Types.ObjectId,
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
},
|
||||
avatar: {
|
||||
type: String,
|
||||
},
|
||||
likes: [
|
||||
{
|
||||
user: {
|
||||
type: Schema.Types.ObjectId,
|
||||
},
|
||||
},
|
||||
],
|
||||
comments: [
|
||||
{
|
||||
user: {
|
||||
type: Schema.Types.ObjectId,
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
},
|
||||
avatar: {
|
||||
type: String,
|
||||
},
|
||||
date: {
|
||||
type: Date,
|
||||
default: Date.now,
|
||||
},
|
||||
},
|
||||
],
|
||||
date: {
|
||||
type: Date,
|
||||
default: Date.now,
|
||||
},
|
||||
});
|
||||
|
||||
module.exports = mongoose.model("post", PostSchema);
|
||||
Executable
+113
@@ -0,0 +1,113 @@
|
||||
const mongoose = require("mongoose");
|
||||
|
||||
const ProfileSchema = new mongoose.Schema({
|
||||
user: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: "user",
|
||||
},
|
||||
company: {
|
||||
type: String,
|
||||
},
|
||||
website: {
|
||||
type: String,
|
||||
},
|
||||
location: {
|
||||
type: String,
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
skills: {
|
||||
type: [String],
|
||||
required: true,
|
||||
},
|
||||
bio: {
|
||||
type: String,
|
||||
},
|
||||
githubusername: {
|
||||
type: String,
|
||||
},
|
||||
experience: [
|
||||
{
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
company: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
location: {
|
||||
type: String,
|
||||
},
|
||||
from: {
|
||||
type: Date,
|
||||
required: true,
|
||||
},
|
||||
to: {
|
||||
type: Date,
|
||||
},
|
||||
current: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
],
|
||||
education: [
|
||||
{
|
||||
school: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
degree: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
fieldofstudy: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
from: {
|
||||
type: Date,
|
||||
required: true,
|
||||
},
|
||||
to: {
|
||||
type: Date,
|
||||
},
|
||||
current: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
],
|
||||
social: {
|
||||
youtube: {
|
||||
type: String,
|
||||
},
|
||||
twitter: {
|
||||
type: String,
|
||||
},
|
||||
facebook: {
|
||||
type: String,
|
||||
},
|
||||
linkedin: {
|
||||
type: String,
|
||||
},
|
||||
instagram: {
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
date: {
|
||||
type: Date,
|
||||
default: Date.now,
|
||||
},
|
||||
});
|
||||
|
||||
module.exports = mongoose.model("profile", ProfileSchema);
|
||||
Executable
+26
@@ -0,0 +1,26 @@
|
||||
const mongoose = require("mongoose");
|
||||
|
||||
const UserSchema = new mongoose.Schema({
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
email: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true,
|
||||
},
|
||||
password: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
avatar: {
|
||||
type: String,
|
||||
},
|
||||
date: {
|
||||
type: Date,
|
||||
default: Date.now,
|
||||
},
|
||||
});
|
||||
|
||||
module.exports = mongoose.model("user", UserSchema);
|
||||
Executable
+80
@@ -0,0 +1,80 @@
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
const bcrypt = require("bcryptjs");
|
||||
const auth = require("../../middleware/auth");
|
||||
const jwt = require("jsonwebtoken");
|
||||
const config = require("config");
|
||||
const { check, validationResult } = require("express-validator");
|
||||
|
||||
const User = require("../../models/User");
|
||||
|
||||
// @route GET api/auth
|
||||
// @desc Get user by token
|
||||
// @access Private
|
||||
router.get("/", auth, async (req, res) => {
|
||||
try {
|
||||
const user = await User.findById(req.user.id).select("-password");
|
||||
res.json(user);
|
||||
} catch (err) {
|
||||
console.error(err.message);
|
||||
res.status(500).send("Server Error");
|
||||
}
|
||||
});
|
||||
|
||||
// @route POST api/auth
|
||||
// @desc Authenticate user & get token
|
||||
// @access Public
|
||||
router.post(
|
||||
"/",
|
||||
[
|
||||
check("email", "Please include a valid email").isEmail(),
|
||||
check("password", "Password is required").exists(),
|
||||
],
|
||||
async (req, res) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({ errors: errors.array() });
|
||||
}
|
||||
|
||||
const { email, password } = req.body;
|
||||
|
||||
try {
|
||||
let user = await User.findOne({ email });
|
||||
|
||||
if (!user) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ errors: [{ msg: "Invalid Credentials" }] });
|
||||
}
|
||||
|
||||
const isMatch = await bcrypt.compare(password, user.password);
|
||||
|
||||
if (!isMatch) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ errors: [{ msg: "Invalid Credentials" }] });
|
||||
}
|
||||
|
||||
const payload = {
|
||||
user: {
|
||||
id: user.id,
|
||||
},
|
||||
};
|
||||
|
||||
jwt.sign(
|
||||
payload,
|
||||
config.get("jwtSecret"),
|
||||
{ expiresIn: 360000 },
|
||||
(err, token) => {
|
||||
if (err) throw err;
|
||||
res.json({ token });
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(err.message);
|
||||
res.status(500).send("Server error");
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
Executable
+221
@@ -0,0 +1,221 @@
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
const { check, validationResult } = require("express-validator");
|
||||
const auth = require("../../middleware/auth");
|
||||
|
||||
const Post = require("../../models/Post");
|
||||
const User = require("../../models/User");
|
||||
const checkObjectId = require("../../middleware/checkObjectId");
|
||||
|
||||
// @route POST api/posts
|
||||
// @desc Create a post
|
||||
// @access Private
|
||||
|
||||
router.post(
|
||||
"/",
|
||||
auth,
|
||||
check("text", "Text is required").notEmpty(),
|
||||
async (req, res) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({ errors: errors.array() });
|
||||
}
|
||||
|
||||
try {
|
||||
const user = await User.findById(req.user.id).select("-password");
|
||||
|
||||
const newPost = new Post({
|
||||
text: req.body.text,
|
||||
name: user.name,
|
||||
avatar: user.avatar,
|
||||
user: req.user.id,
|
||||
});
|
||||
|
||||
const post = await newPost.save();
|
||||
|
||||
res.json(post);
|
||||
} catch (err) {
|
||||
console.error(err.message);
|
||||
res.status(500).send("Server Error");
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// @route GET api/posts
|
||||
// @desc Get all posts
|
||||
// @access Private
|
||||
router.get("/", auth, async (req, res) => {
|
||||
try {
|
||||
const posts = await Post.find().sort({ date: -1 });
|
||||
res.json(posts);
|
||||
} catch (err) {
|
||||
console.error(err.message);
|
||||
res.status(500).send("Server Error");
|
||||
}
|
||||
});
|
||||
|
||||
// @route GET api/posts/:id
|
||||
// @desc Get post by ID
|
||||
// @access Private
|
||||
router.get("/:id", auth, checkObjectId("id"), async (req, res) => {
|
||||
try {
|
||||
const post = await Post.findById(req.params.id);
|
||||
|
||||
if (!post) {
|
||||
return res.status(404).json({ msg: "Post not found" });
|
||||
}
|
||||
|
||||
res.json(post);
|
||||
} catch (err) {
|
||||
console.error(err.message);
|
||||
|
||||
res.status(500).send("Server Error");
|
||||
}
|
||||
});
|
||||
|
||||
// @route DELETE api/posts/:id
|
||||
// @desc Delete a post
|
||||
// @access Private
|
||||
router.delete("/:id", [auth, checkObjectId("id")], async (req, res) => {
|
||||
try {
|
||||
const post = await Post.findById(req.params.id);
|
||||
|
||||
if (!post) {
|
||||
return res.status(404).json({ msg: "Post not found" });
|
||||
}
|
||||
|
||||
// Check user
|
||||
if (post.user.toString() !== req.user.id) {
|
||||
return res.status(401).json({ msg: "User not authorized" });
|
||||
}
|
||||
|
||||
await post.remove();
|
||||
|
||||
res.json({ msg: "Post removed" });
|
||||
} catch (err) {
|
||||
console.error(err.message);
|
||||
|
||||
res.status(500).send("Server Error");
|
||||
}
|
||||
});
|
||||
|
||||
// @route PUT api/posts/like/:id
|
||||
// @desc Like a post
|
||||
// @access Private
|
||||
router.put("/like/:id", auth, checkObjectId("id"), async (req, res) => {
|
||||
try {
|
||||
const post = await Post.findById(req.params.id);
|
||||
|
||||
// Check if the post has already been liked
|
||||
if (post.likes.some((like) => like.user.toString() === req.user.id)) {
|
||||
return res.status(400).json({ msg: "Post already liked" });
|
||||
}
|
||||
|
||||
post.likes.unshift({ user: req.user.id });
|
||||
|
||||
await post.save();
|
||||
|
||||
return res.json(post.likes);
|
||||
} catch (err) {
|
||||
console.error(err.message);
|
||||
res.status(500).send("Server Error");
|
||||
}
|
||||
});
|
||||
|
||||
// @route PUT api/posts/unlike/:id
|
||||
// @desc Unlike a post
|
||||
// @access Private
|
||||
router.put("/unlike/:id", auth, checkObjectId("id"), async (req, res) => {
|
||||
try {
|
||||
const post = await Post.findById(req.params.id);
|
||||
|
||||
// Check if the post has not yet been liked
|
||||
if (!post.likes.some((like) => like.user.toString() === req.user.id)) {
|
||||
return res.status(400).json({ msg: "Post has not yet been liked" });
|
||||
}
|
||||
|
||||
// remove the like
|
||||
post.likes = post.likes.filter(
|
||||
({ user }) => user.toString() !== req.user.id
|
||||
);
|
||||
|
||||
await post.save();
|
||||
|
||||
return res.json(post.likes);
|
||||
} catch (err) {
|
||||
console.error(err.message);
|
||||
res.status(500).send("Server Error");
|
||||
}
|
||||
});
|
||||
|
||||
// @route POST api/posts/comment/:id
|
||||
// @desc Comment on a post
|
||||
// @access Private
|
||||
router.post(
|
||||
"/comment/:id",
|
||||
auth,
|
||||
checkObjectId("id"),
|
||||
check("text", "Text is required").notEmpty(),
|
||||
async (req, res) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({ errors: errors.array() });
|
||||
}
|
||||
|
||||
try {
|
||||
const user = await User.findById(req.user.id).select("-password");
|
||||
const post = await Post.findById(req.params.id);
|
||||
|
||||
const newComment = {
|
||||
text: req.body.text,
|
||||
name: user.name,
|
||||
avatar: user.avatar,
|
||||
user: req.user.id,
|
||||
};
|
||||
|
||||
post.comments.unshift(newComment);
|
||||
|
||||
await post.save();
|
||||
|
||||
res.json(post.comments);
|
||||
} catch (err) {
|
||||
console.error(err.message);
|
||||
res.status(500).send("Server Error");
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// @route DELETE api/posts/comment/:id/:comment_id
|
||||
// @desc Delete comment
|
||||
// @access Private
|
||||
router.delete("/comment/:id/:comment_id", auth, async (req, res) => {
|
||||
try {
|
||||
const post = await Post.findById(req.params.id);
|
||||
|
||||
// Pull out comment
|
||||
const comment = post.comments.find(
|
||||
(comment) => comment.id === req.params.comment_id
|
||||
);
|
||||
// Make sure comment exists
|
||||
if (!comment) {
|
||||
return res.status(404).json({ msg: "Comment does not exist" });
|
||||
}
|
||||
// Check user
|
||||
if (comment.user.toString() !== req.user.id) {
|
||||
return res.status(401).json({ msg: "User not authorized" });
|
||||
}
|
||||
|
||||
post.comments = post.comments.filter(
|
||||
({ id }) => id !== req.params.comment_id
|
||||
);
|
||||
|
||||
await post.save();
|
||||
|
||||
return res.json(post.comments);
|
||||
} catch (err) {
|
||||
console.error(err.message);
|
||||
return res.status(500).send("Server Error");
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
Executable
+280
@@ -0,0 +1,280 @@
|
||||
const express = require('express');
|
||||
const axios = require('axios');
|
||||
const config = require('config');
|
||||
const router = express.Router();
|
||||
const auth = require('../../middleware/auth');
|
||||
const { check, validationResult } = require('express-validator');
|
||||
// bring in normalize to give us a proper url, regardless of what user entered
|
||||
const normalize = require('normalize-url');
|
||||
const checkObjectId = require('../../middleware/checkObjectId');
|
||||
|
||||
const Profile = require('../../models/Profile');
|
||||
const User = require('../../models/User');
|
||||
const Post = require('../../models/Post');
|
||||
// @route GET api/profile/me
|
||||
// @desc Get current users profile
|
||||
// @access Private
|
||||
router.get('/me', auth, async (req, res) => {
|
||||
try {
|
||||
const profile = await Profile.findOne({
|
||||
user: req.user.id
|
||||
}).populate('user', ['name', 'avatar']);
|
||||
|
||||
if (!profile) {
|
||||
return res.status(400).json({ msg: 'There is no profile for this user' });
|
||||
}
|
||||
|
||||
res.json(profile);
|
||||
} catch (err) {
|
||||
console.error(err.message);
|
||||
res.status(500).send('Server Error');
|
||||
}
|
||||
});
|
||||
|
||||
// @route POST api/profile
|
||||
// @desc Create or update user profile
|
||||
// @access Private
|
||||
router.post(
|
||||
'/',
|
||||
auth,
|
||||
check('status', 'Status is required').notEmpty(),
|
||||
check('skills', 'Skills is required').notEmpty(),
|
||||
async (req, res) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({ errors: errors.array() });
|
||||
}
|
||||
|
||||
// destructure the request
|
||||
const {
|
||||
website,
|
||||
skills,
|
||||
youtube,
|
||||
twitter,
|
||||
instagram,
|
||||
linkedin,
|
||||
facebook,
|
||||
// spread the rest of the fields we don't need to check
|
||||
...rest
|
||||
} = req.body;
|
||||
|
||||
// build a profile
|
||||
const profileFields = {
|
||||
user: req.user.id,
|
||||
website:
|
||||
website && website !== ''
|
||||
? normalize(website, { forceHttps: true })
|
||||
: '',
|
||||
skills: Array.isArray(skills)
|
||||
? skills
|
||||
: skills.split(',').map((skill) => ' ' + skill.trim()),
|
||||
...rest
|
||||
};
|
||||
|
||||
// Build socialFields object
|
||||
const socialFields = { youtube, twitter, instagram, linkedin, facebook };
|
||||
|
||||
// normalize social fields to ensure valid url
|
||||
for (const [key, value] of Object.entries(socialFields)) {
|
||||
if (value && value.length > 0)
|
||||
socialFields[key] = normalize(value, { forceHttps: true });
|
||||
}
|
||||
// add to profileFields
|
||||
profileFields.social = socialFields;
|
||||
|
||||
try {
|
||||
// Using upsert option (creates new doc if no match is found):
|
||||
let profile = await Profile.findOneAndUpdate(
|
||||
{ user: req.user.id },
|
||||
{ $set: profileFields },
|
||||
{ new: true, upsert: true, setDefaultsOnInsert: true }
|
||||
);
|
||||
return res.json(profile);
|
||||
} catch (err) {
|
||||
console.error(err.message);
|
||||
return res.status(500).send('Server Error');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// @route GET api/profile
|
||||
// @desc Get all profiles
|
||||
// @access Public
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const profiles = await Profile.find().populate('user', ['name', 'avatar']);
|
||||
res.json(profiles);
|
||||
} catch (err) {
|
||||
console.error(err.message);
|
||||
res.status(500).send('Server Error');
|
||||
}
|
||||
});
|
||||
|
||||
// @route GET api/profile/user/:user_id
|
||||
// @desc Get profile by user ID
|
||||
// @access Public
|
||||
router.get(
|
||||
'/user/:user_id',
|
||||
checkObjectId('user_id'),
|
||||
async ({ params: { user_id } }, res) => {
|
||||
try {
|
||||
const profile = await Profile.findOne({
|
||||
user: user_id
|
||||
}).populate('user', ['name', 'avatar']);
|
||||
|
||||
if (!profile) return res.status(400).json({ msg: 'Profile not found' });
|
||||
|
||||
return res.json(profile);
|
||||
} catch (err) {
|
||||
console.error(err.message);
|
||||
return res.status(500).json({ msg: 'Server error' });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// @route DELETE api/profile
|
||||
// @desc Delete profile, user & posts
|
||||
// @access Private
|
||||
router.delete('/', auth, async (req, res) => {
|
||||
try {
|
||||
// Remove user posts
|
||||
// Remove profile
|
||||
// Remove user
|
||||
await Promise.all([
|
||||
Post.deleteMany({ user: req.user.id }),
|
||||
Profile.findOneAndRemove({ user: req.user.id }),
|
||||
User.findOneAndRemove({ _id: req.user.id })
|
||||
]);
|
||||
|
||||
res.json({ msg: 'User deleted' });
|
||||
} catch (err) {
|
||||
console.error(err.message);
|
||||
res.status(500).send('Server Error');
|
||||
}
|
||||
});
|
||||
|
||||
// @route PUT api/profile/experience
|
||||
// @desc Add profile experience
|
||||
// @access Private
|
||||
router.put(
|
||||
'/experience',
|
||||
auth,
|
||||
check('title', 'Title is required').notEmpty(),
|
||||
check('company', 'Company is required').notEmpty(),
|
||||
check('from', 'From date is required and needs to be from the past')
|
||||
.notEmpty()
|
||||
.custom((value, { req }) => (req.body.to ? value < req.body.to : true)),
|
||||
async (req, res) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({ errors: errors.array() });
|
||||
}
|
||||
|
||||
try {
|
||||
const profile = await Profile.findOne({ user: req.user.id });
|
||||
|
||||
profile.experience.unshift(req.body);
|
||||
|
||||
await profile.save();
|
||||
|
||||
res.json(profile);
|
||||
} catch (err) {
|
||||
console.error(err.message);
|
||||
res.status(500).send('Server Error');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// @route DELETE api/profile/experience/:exp_id
|
||||
// @desc Delete experience from profile
|
||||
// @access Private
|
||||
|
||||
router.delete('/experience/:exp_id', auth, async (req, res) => {
|
||||
try {
|
||||
const foundProfile = await Profile.findOne({ user: req.user.id });
|
||||
|
||||
foundProfile.experience = foundProfile.experience.filter(
|
||||
(exp) => exp._id.toString() !== req.params.exp_id
|
||||
);
|
||||
|
||||
await foundProfile.save();
|
||||
return res.status(200).json(foundProfile);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return res.status(500).json({ msg: 'Server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// @route PUT api/profile/education
|
||||
// @desc Add profile education
|
||||
// @access Private
|
||||
router.put(
|
||||
'/education',
|
||||
auth,
|
||||
check('school', 'School is required').notEmpty(),
|
||||
check('degree', 'Degree is required').notEmpty(),
|
||||
check('fieldofstudy', 'Field of study is required').notEmpty(),
|
||||
check('from', 'From date is required and needs to be from the past')
|
||||
.notEmpty()
|
||||
.custom((value, { req }) => (req.body.to ? value < req.body.to : true)),
|
||||
async (req, res) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({ errors: errors.array() });
|
||||
}
|
||||
|
||||
try {
|
||||
const profile = await Profile.findOne({ user: req.user.id });
|
||||
|
||||
profile.education.unshift(req.body);
|
||||
|
||||
await profile.save();
|
||||
|
||||
res.json(profile);
|
||||
} catch (err) {
|
||||
console.error(err.message);
|
||||
res.status(500).send('Server Error');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// @route DELETE api/profile/education/:edu_id
|
||||
// @desc Delete education from profile
|
||||
// @access Private
|
||||
|
||||
router.delete('/education/:edu_id', auth, async (req, res) => {
|
||||
try {
|
||||
const foundProfile = await Profile.findOne({ user: req.user.id });
|
||||
foundProfile.education = foundProfile.education.filter(
|
||||
(edu) => edu._id.toString() !== req.params.edu_id
|
||||
);
|
||||
await foundProfile.save();
|
||||
return res.status(200).json(foundProfile);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return res.status(500).json({ msg: 'Server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// @route GET api/profile/github/:username
|
||||
// @desc Get user repos from Github
|
||||
// @access Public
|
||||
router.get('/github/:username', async (req, res) => {
|
||||
try {
|
||||
const uri = encodeURI(
|
||||
`https://api.github.com/users/${req.params.username}/repos?per_page=5&sort=created:asc`
|
||||
);
|
||||
const headers = {
|
||||
'user-agent': 'node.js',
|
||||
Authorization: `token ${config.get('githubToken')}`
|
||||
};
|
||||
|
||||
const gitHubResponse = await axios.get(uri, { headers });
|
||||
return res.json(gitHubResponse.data);
|
||||
} catch (err) {
|
||||
console.error(err.message);
|
||||
return res.status(404).json({ msg: 'No Github profile found' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
Executable
+83
@@ -0,0 +1,83 @@
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
const gravatar = require("gravatar");
|
||||
const bcrypt = require("bcryptjs");
|
||||
const jwt = require("jsonwebtoken");
|
||||
const config = require("config");
|
||||
const { check, validationResult } = require("express-validator");
|
||||
const User = require("../../models/User");
|
||||
const normalize = require('normalize-url');
|
||||
|
||||
// @route POST api/users
|
||||
// @desc Register user
|
||||
// @access Public
|
||||
router.post(
|
||||
"/",
|
||||
check("name", "Name is required").notEmpty(),
|
||||
check("email", "Please include a valid email").isEmail(),
|
||||
check(
|
||||
"password",
|
||||
"Please enter a password with 6 or more characters"
|
||||
).isLength({ min: 6 }),
|
||||
async (req, res) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({ errors: errors.array() });
|
||||
}
|
||||
|
||||
const { name, email, password } = req.body;
|
||||
|
||||
try {
|
||||
let user = await User.findOne({ email });
|
||||
|
||||
if (user) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ errors: [{ msg: "User already exists" }] });
|
||||
}
|
||||
|
||||
const avatar = normalize(
|
||||
gravatar.url(email, {
|
||||
s: "200",
|
||||
r: "pg",
|
||||
d: "mm",
|
||||
}),
|
||||
{ forceHttps: true }
|
||||
);
|
||||
|
||||
user = new User({
|
||||
name,
|
||||
email,
|
||||
avatar,
|
||||
password,
|
||||
});
|
||||
|
||||
const salt = await bcrypt.genSalt(10);
|
||||
|
||||
user.password = await bcrypt.hash(password, salt);
|
||||
|
||||
await user.save();
|
||||
|
||||
const payload = {
|
||||
user: {
|
||||
id: user.id,
|
||||
},
|
||||
};
|
||||
|
||||
jwt.sign(
|
||||
payload,
|
||||
config.get("jwtSecret"),
|
||||
{ expiresIn: "5 days" },
|
||||
(err, token) => {
|
||||
if (err) throw err;
|
||||
res.json({ token });
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(err.message);
|
||||
res.status(500).send("Server error");
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,27 @@
|
||||
const express = require('express');
|
||||
const connectDB= require('./config/db')
|
||||
|
||||
const path = require('path')
|
||||
const app = express();
|
||||
|
||||
connectDB();
|
||||
|
||||
app.use(express.json({extended:false}));
|
||||
|
||||
|
||||
app.use('/api/users', require('./routers/api/users'))
|
||||
app.use('/api/auth', require('./routers/api/auth'))
|
||||
app.use('/api/profile', require('./routers/api/profile'))
|
||||
app.use('/api/posts', require('./routers/api/posts'))
|
||||
|
||||
// Serve static assets in production
|
||||
if (process.env.NODE_ENV==='production'){
|
||||
app.use(express.static('client/build'));
|
||||
app.get('*',(req, res)=>[
|
||||
res.sendFile(path.resolve(__dirname, 'client', 'build','index.html'))
|
||||
])
|
||||
}
|
||||
|
||||
const PORT = process.env.PORT || 5000;
|
||||
|
||||
app.listen(PORT,()=> console.log(`Server started on port ${PORT}`));
|
||||
Reference in New Issue
Block a user