frontend converted to ts

This commit is contained in:
QkoSad
2023-07-25 20:40:02 +03:00
parent 1e64a910cc
commit 3bf4e9fc56
60 changed files with 3584 additions and 7 deletions
-3
View File
@@ -1,4 +1 @@
node_modules
default.json
.vscode
TODO.txt
+13 -4
View File
@@ -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"
}
}
+96
View File
@@ -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;
+10
View File
@@ -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);
};
+74
View File
@@ -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);
};
+157
View File
@@ -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 })
);
}
};
}
}
+211
View File
@@ -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)
}
}
};
+62
View File
@@ -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;
+86
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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;
+16
View File
@@ -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;
+34
View File
@@ -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;
+57
View File
@@ -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;
+14
View File
@@ -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;
+14
View File
@@ -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;
+39
View File
@@ -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
+42
View File
@@ -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;
+39
View File
@@ -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;
+36
View File
@@ -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;
+58
View File
@@ -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;
+34
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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;
+89
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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;
+47
View File
@@ -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
View File
@@ -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;
+44
View File
@@ -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
View File
@@ -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;
+5
View File
@@ -0,0 +1,5 @@
declare module "*.gif" {
const path: string;
export default path
}
// bonkers have no idea how this works
+13
View File
@@ -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>
);
+24
View File
@@ -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;
+85
View File
@@ -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;
+109
View File
@@ -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;
+84
View File
@@ -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;
+38
View File
@@ -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;
+93
View File
@@ -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
}
+20
View File
@@ -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;
+5
View File
@@ -0,0 +1,5 @@
function formatDate(date: string) {
return new Intl.DateTimeFormat().format(new Date(date));
}
export default formatDate;
+8
View File
@@ -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
+15
View File
@@ -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;
+26
View File
@@ -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
View File
@@ -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;
+28
View File
@@ -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' });
}
};
+9
View File
@@ -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
View File
@@ -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);
+113
View File
@@ -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
View File
@@ -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);
+80
View File
@@ -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;
+221
View File
@@ -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;
+280
View File
@@ -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;
+83
View File
@@ -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;
Executable
+27
View File
@@ -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}`));