project done

This commit is contained in:
QkoSad
2022-11-07 17:15:55 +02:00
commit 543d271672
68 changed files with 37051 additions and 0 deletions
+584
View File
@@ -0,0 +1,584 @@
/* Global Styles */
:root {
--primary-color: #17a2b8;
--dark-color: #343a40;
--light-color: #f4f4f4;
--danger-color: #dc3545;
--success-color: #28a745;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Raleway', sans-serif;
font-size: 1rem;
line-height: 1.6;
background-color: #fff;
color: #333;
}
a {
color: var(--primary-color);
text-decoration: none;
}
ul {
list-style: none;
}
img {
width: 100%;
}
/* Utilities */
.container {
max-width: 1100px;
margin: auto;
overflow: hidden;
padding: 0 2rem;
margin-top: 6rem;
margin-bottom: 3rem;
}
/* Text Styles*/
.x-large {
font-size: 4rem;
line-height: 1.2;
margin-bottom: 1rem;
}
.large {
font-size: 3rem;
line-height: 1.2;
margin-bottom: 1rem;
}
.lead {
font-size: 1.5rem;
margin-bottom: 1rem;
}
.text-center {
text-align: center;
}
.text-primary {
color: var(--primary-color);
}
.text-dark {
color: var(--dark-color);
}
/* Padding */
.p {
padding: 0.5rem;
}
.p-1 {
padding: 1rem;
}
.p-2 {
padding: 2rem;
}
.p-3 {
padding: 3rem;
}
.py {
padding: 0.5rem 0;
}
.py-1 {
padding: 1rem 0;
}
.py-2 {
padding: 2rem 0;
}
.py-3 {
padding: 3rem 0;
}
/* Margin */
.m {
margin: 0.5rem;
}
.m-1 {
margin: 1rem;
}
.m-2 {
margin: 2rem;
}
.m-3 {
margin: 3rem;
}
.my {
margin: 0.5rem 0;
}
.my-1 {
margin: 1rem 0;
}
.my-2 {
margin: 2rem 0;
}
.my-3 {
margin: 3rem 0;
}
.btn {
display: inline-block;
background: var(--light-color);
color: #333;
padding: 0.4rem 1.3rem;
font-size: 1rem;
border: none;
cursor: pointer;
margin-right: 0.5rem;
transition: opacity 0.2s ease-in;
outline: none;
}
.badge {
font-size: 0.8rem;
padding: 0.1rem;
text-align: center;
margin: 0.3rem;
background: var(--light-color);
color: #333;
}
.alert {
padding: 0.8rem;
margin: 1rem 0;
opacity: 0.9;
background: var(--light-color);
color: #333;
}
.btn-primary,
.bg-primary,
.badge-primary,
.alert-primary {
background: var(--primary-color);
color: #fff;
}
.btn-light,
.bg-light,
.badge-light,
.alert-light {
background: var(--light-color);
color: #333;
}
.btn-dark,
.bg-dark,
.badge-dark,
.alert-dark {
background: var(--dark-color);
color: #fff;
}
.btn-danger,
.bg-danger,
.badge-danger,
.alert-danger {
background: var(--danger-color);
color: #fff;
}
.btn-success,
.bg-success,
.badge-success,
.alert-success {
background: var(--success-color);
color: #fff;
}
.btn-white,
.bg-white,
.badge-white,
.alert-white {
background: #fff;
color: #333;
border: #ccc solid 1px;
}
.btn:hover {
opacity: 0.8;
}
.bg-light,
.badge-light {
border: #ccc solid 1px;
}
.round-img {
border-radius: 50%;
}
.line {
height: 1px;
background: #ccc;
margin: 1.5rem 0;
}
/* Overlay */
.dark-overlay {
background-color: rgba(0, 0, 0, 0.7);
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
/* Forms */
.form .form-group {
margin: 1.2rem 0;
}
.form .form-text {
display: block;
margin-top: 0.3rem;
color: #888;
}
.form input[type='text'],
.form input[type='email'],
.form input[type='password'],
.form input[type='date'],
.form select,
.form textarea {
display: block;
width: 100%;
padding: 0.4rem;
font-size: 1.2rem;
border: 1px solid #ccc;
}
.form input[type='submit'],
button {
font: inherit;
}
.form .social-input {
display: flex;
}
.form .social-input i {
padding: 0.5rem;
width: 4rem;
}
.form .social-input i.fa-twitter {
color: #38a1f3;
}
.form .social-input i.fa-facebook {
color: #3b5998;
}
.form .social-input i.fa-instagram {
color: #3f729b;
}
.form .social-input i.fa-youtube {
color: #c4302b;
}
.form .social-input i.fa-linkedin {
color: #0077b5;
}
.table th,
.table td {
padding: 1rem;
text-align: left;
}
.table th {
background: var(--light-color);
}
/* Navbar */
.navbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.7rem 2rem;
position: fixed;
z-index: 1;
width: 100%;
top: 0;
border-bottom: solid 1px var(--primary-color);
opacity: 0.9;
}
.navbar ul {
display: flex;
}
.navbar a {
color: #fff;
padding: 0.45rem;
margin: 0 0.25rem;
}
.navbar a:hover {
color: var(--primary-color);
}
.navbar .welcome span {
margin-right: 0.6rem;
}
/* Landing Page */
.landing {
position: relative;
background: url('./img/vimCheatSheet.jpg') no-repeat center center/cover;
height: 100vh;
}
.landing-inner {
color: #fff;
height: 100%;
width: 80%;
margin: auto;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
}
/* Profiles Page */
.profile {
display: grid;
grid-template-columns: 2fr 4fr 2fr;
align-items: center;
grid-gap: 2rem;
padding: 1rem;
line-height: 1.8;
margin-bottom: 1rem;
}
/* Profile Page */
.profile-grid {
display: grid;
grid-template-areas:
'top top'
'about about'
'exp edu'
'github github';
grid-gap: 1rem;
}
.profile-top {
grid-area: top;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
}
.profile-top img {
width: 250px;
}
.profile-top .icons a {
color: #fff;
margin: 0 0.3rem;
}
.profile-top .icons a:hover {
color: var(--dark-color);
}
.profile-about {
grid-area: about;
text-align: center;
}
.profile-about .skills {
display: flex;
align-items: center;
justify-content: center;
text-align: center;
}
.profile-exp {
grid-area: exp;
}
.profile-edu {
grid-area: edu;
}
.profile-exp h2,
.profile-edu h2 {
margin-bottom: 1rem;
}
.profile-exp > div,
.profile-edu > div {
margin-bottom: 1rem;
padding-bottom: 1rem;
border-bottom: #ccc 1px dotted;
}
.profile-exp > div:last-child,
.profile-edu > div:last-child {
border: 0;
}
.profile-exp p,
.profile-edu p {
margin: 0.5rem 0;
}
.profile-github {
grid-area: github;
}
.profile-github .repo {
display: flex;
}
.profile-github .repo > div:first-child {
flex: 7;
flex-basis: 70%;
}
.profile-github > div:last-child {
flex: 3;
flex-basis: 20%;
}
/* Posts Page */
.post-form .post-form-header {
background: var(--primary-color);
padding: 0.5rem;
}
.post {
display: grid;
grid-template-columns: 1fr 4fr;
grid-gap: 2rem;
align-items: center;
}
.post > div:first-child {
text-align: center;
}
.post img {
width: 100px;
}
.post .comment-count {
background: var(--light-color);
color: var(--primary-color);
padding: 0.1rem 0.2rem;
border-radius: 5px;
font-size: 0.8rem;
}
.post .post-date {
color: #aaa;
font-size: 0.8rem;
margin-bottom: 0.5rem;
}
/* Mobile Styles */
@media (max-width: 700px) {
.container {
margin-top: 8rem;
}
.hide-sm {
display: none;
}
/* Text Styles */
.x-large {
font-size: 3rem;
}
.large {
font-size: 2rem;
}
.lead {
font-size: 1rem;
}
/* Navbar */
.navbar {
display: block;
text-align: center;
}
.navbar ul {
text-align: center;
justify-content: center;
}
.navbar h1 {
margin-bottom: 1rem;
}
.navbar .welcome {
display: none;
}
/* Profiles Page */
.profile {
grid-template-columns: 1fr;
text-align: center;
}
.profile ul {
display: none;
}
/* Profile Page */
.profile-top img,
.profile img {
width: 200px;
margin: auto;
}
.profile-grid {
grid-template-areas:
'top'
'about'
'exp'
'edu'
'github';
}
.profile-about .skills {
flex-direction: column;
}
.dash-buttons a {
display: block;
width: 100%;
margin-bottom: 0.2rem;
}
.post {
grid-template-columns: 1fr;
}
.post a,
.post button {
padding: 0.3rem 0.4rem;
}
}
.alert-wrapper {
position: fixed;
top: 4rem;
right: 2rem;
display: inline-block;
}
+85
View File
@@ -0,0 +1,85 @@
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";
import { LOGOUT } from "./actions/types";
// Redux
import { Provider } from "react-redux";
import store from "./store";
import { loadUser } from "./actions/auth";
import setAuthToken from "./utils/setAuthToken";
import "./App.css";
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({ type: 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="porifles/: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>
);
};
export default App;
+12
View File
@@ -0,0 +1,12 @@
import { v4 as uuidv4 } from 'uuid';
import { SET_ALERT, REMOVE_ALERT } from './types';
export const setAlert = (msg, alertType, timeout = 5000) => dispatch => {
const id = uuidv4();
dispatch({
type: SET_ALERT,
payload: { msg, alertType, id }
});
setTimeout(() => dispatch({ type: REMOVE_ALERT, payload: id }), timeout);
};
+86
View File
@@ -0,0 +1,86 @@
import api from "../utils/api";
import { setAlert } from "./alert";
import {
REGISTER_SUCCESS,
REGISTER_FAIL,
USER_LOADED,
AUTH_ERROR,
LOGIN_SUCCESS,
LOGIN_FAIL,
LOGOUT,
} from "./types";
/*
NOTE: we don't need a config object for axios as the
default headers in axios are already Content-Type: application/json
also axios stringifies and parses JSON for you, so no need for
JSON.stringify or JSON.parse
*/
// Load User
export const loadUser = () => async (dispatch) => {
try {
const res = await api.get("/auth");
dispatch({
type: USER_LOADED,
payload: res.data,
});
} catch (err) {
dispatch({
type: AUTH_ERROR,
});
}
};
// Register User
export const register = (formData) => async (dispatch) => {
try {
const res = await api.post("/users", formData);
dispatch({
type: REGISTER_SUCCESS,
payload: res.data,
});
dispatch(loadUser());
} catch (err) {
const errors = err.response.data.errors;
if (errors) {
errors.forEach((error) => dispatch(setAlert(error.msg, "danger")));
}
dispatch({
type: REGISTER_FAIL,
});
}
};
// Login User
export const login = (email, password) => async (dispatch) => {
const body = { email, password };
try {
const res = await api.post("/auth", body);
dispatch({
type: LOGIN_SUCCESS,
payload: res.data,
});
dispatch(loadUser());
} catch (err) {
const errors = err.response.data.errors;
if (errors) {
errors.forEach((error) => dispatch(setAlert(error.msg, "danger")));
}
dispatch({
type: LOGIN_FAIL,
});
}
};
// Logout
export const logout = () => ({ type: LOGOUT });
+163
View File
@@ -0,0 +1,163 @@
import api from '../utils/api';
import { setAlert } from './alert';
import {
GET_POSTS,
POST_ERROR,
UPDATE_LIKES,
DELETE_POST,
ADD_POST,
GET_POST,
ADD_COMMENT,
REMOVE_COMMENT
} from './types';
/*
NOTE: we don't need a config object for axios as the
default headers in axios are already Content-Type: application/json
also axios stringifies and parses JSON for you, so no need for
JSON.stringify or JSON.parse
*/
// Get posts
export const getPosts = () => async (dispatch) => {
try {
const res = await api.get('/posts');
dispatch({
type: GET_POSTS,
payload: res.data
});
} catch (err) {
dispatch({
type: POST_ERROR,
payload: { msg: err.response.statusText, status: err.response.status }
});
}
};
// Add like
export const addLike = (id) => async (dispatch) => {
try {
const res = await api.put(`/posts/like/${id}`);
dispatch({
type: UPDATE_LIKES,
payload: { id, likes: res.data }
});
} catch (err) {
dispatch({
type: POST_ERROR,
payload: { msg: err.response.statusText, status: err.response.status }
});
}
};
// Remove like
export const removeLike = (id) => async (dispatch) => {
try {
const res = await api.put(`/posts/unlike/${id}`);
dispatch({
type: UPDATE_LIKES,
payload: { id, likes: res.data }
});
} catch (err) {
dispatch({
type: POST_ERROR,
payload: { msg: err.response.statusText, status: err.response.status }
});
}
};
// Delete post
export const deletePost = (id) => async (dispatch) => {
try {
await api.delete(`/posts/${id}`);
dispatch({
type: DELETE_POST,
payload: id
});
dispatch(setAlert('Post Removed', 'success'));
} catch (err) {
dispatch({
type: POST_ERROR,
payload: { msg: err.response.statusText, status: err.response.status }
});
}
};
// Add post
export const addPost = (formData) => async (dispatch) => {
try {
const res = await api.post('/posts', formData);
dispatch({
type: ADD_POST,
payload: res.data
});
dispatch(setAlert('Post Created', 'success'));
} catch (err) {
dispatch({
type: POST_ERROR,
payload: { msg: err.response.statusText, status: err.response.status }
});
}
};
// Get post
export const getPost = (id) => async (dispatch) => {
try {
const res = await api.get(`/posts/${id}`);
dispatch({
type: GET_POST,
payload: res.data
});
} catch (err) {
dispatch({
type: POST_ERROR,
payload: { msg: err.response.statusText, status: err.response.status }
});
}
};
// Add comment
export const addComment = (postId, formData) => async (dispatch) => {
try {
const res = await api.post(`/posts/comment/${postId}`, formData);
dispatch({
type: ADD_COMMENT,
payload: res.data
});
dispatch(setAlert('Comment Added', 'success'));
} catch (err) {
dispatch({
type: POST_ERROR,
payload: { msg: err.response.statusText, status: err.response.status }
});
}
};
// Delete comment
export const deleteComment = (postId, commentId) => async (dispatch) => {
try {
await api.delete(`/posts/comment/${postId}/${commentId}`);
dispatch({
type: REMOVE_COMMENT,
payload: commentId
});
dispatch(setAlert('Comment Removed', 'success'));
} catch (err) {
dispatch({
type: POST_ERROR,
payload: { msg: err.response.statusText, status: err.response.status }
});
}
};
+233
View File
@@ -0,0 +1,233 @@
import api from '../utils/api';
import { setAlert } from './alert';
import {
GET_PROFILE,
GET_PROFILES,
PROFILE_ERROR,
UPDATE_PROFILE,
CLEAR_PROFILE,
ACCOUNT_DELETED,
GET_REPOS,
NO_REPOS
} from './types';
/*
NOTE: we don't need a config object for axios as the
default headers in axios are already Content-Type: application/json
also axios stringifies and parses JSON for you, so no need for
JSON.stringify or JSON.parse
*/
// Get current users profile
export const getCurrentProfile = () => async (dispatch) => {
try {
const res = await api.get('/profile/me');
dispatch({
type: GET_PROFILE,
payload: res.data
});
} catch (err) {
dispatch({
type: PROFILE_ERROR,
payload: { msg: err.response.statusText, status: err.response.status }
});
}
};
// Get all profiles
export const getProfiles = () => async (dispatch) => {
dispatch({ type: CLEAR_PROFILE });
try {
const res = await api.get('/profile');
dispatch({
type: GET_PROFILES,
payload: res.data
});
} catch (err) {
dispatch({
type: PROFILE_ERROR,
payload: { msg: err.response.statusText, status: err.response.status }
});
}
};
// Get profile by ID
export const getProfileById = (userId) => async (dispatch) => {
try {
const res = await api.get(`/profile/user/${userId}`);
dispatch({
type: GET_PROFILE,
payload: res.data
});
} catch (err) {
dispatch({
type: PROFILE_ERROR,
payload: { msg: err.response.statusText, status: err.response.status }
});
}
};
// Get Github repos
export const getGithubRepos = (username) => async (dispatch) => {
try {
const res = await api.get(`/profile/github/${username}`);
dispatch({
type: GET_REPOS,
payload: res.data
});
} catch (err) {
dispatch({
type: NO_REPOS
});
}
};
// Create or update profile
export const createProfile =
(formData, navigate, edit = false) =>
async (dispatch) => {
try {
const res = await api.post('/profile', formData);
dispatch({
type: GET_PROFILE,
payload: res.data
});
dispatch(
setAlert(edit ? 'Profile Updated' : 'Profile Created', 'success')
);
if (!edit) {
navigate('/dashboard');
}
} catch (err) {
const errors = err.response.data.errors;
if (errors) {
errors.forEach((error) => dispatch(setAlert(error.msg, 'danger')));
}
dispatch({
type: PROFILE_ERROR,
payload: { msg: err.response.statusText, status: err.response.status }
});
}
};
// Add Experience
export const addExperience = (formData, navigate) => async (dispatch) => {
try {
const res = await api.put('/profile/experience', formData);
dispatch({
type: UPDATE_PROFILE,
payload: res.data
});
dispatch(setAlert('Experience Added', 'success'));
navigate('/dashboard');
} catch (err) {
const errors = err.response.data.errors;
if (errors) {
errors.forEach((error) => dispatch(setAlert(error.msg, 'danger')));
}
dispatch({
type: PROFILE_ERROR,
payload: { msg: err.response.statusText, status: err.response.status }
});
}
};
// Add Education
export const addEducation = (formData, navigate) => async (dispatch) => {
try {
const res = await api.put('/profile/education', formData);
dispatch({
type: UPDATE_PROFILE,
payload: res.data
});
dispatch(setAlert('Education Added', 'success'));
navigate('/dashboard');
} catch (err) {
const errors = err.response.data.errors;
if (errors) {
errors.forEach((error) => dispatch(setAlert(error.msg, 'danger')));
}
dispatch({
type: PROFILE_ERROR,
payload: { msg: err.response.statusText, status: err.response.status }
});
}
};
// Delete experience
export const deleteExperience = (id) => async (dispatch) => {
try {
const res = await api.delete(`/profile/experience/${id}`);
dispatch({
type: UPDATE_PROFILE,
payload: res.data
});
dispatch(setAlert('Experience Removed', 'success'));
} catch (err) {
dispatch({
type: PROFILE_ERROR,
payload: { msg: err.response.statusText, status: err.response.status }
});
}
};
// Delete education
export const deleteEducation = (id) => async (dispatch) => {
try {
const res = await api.delete(`/profile/education/${id}`);
dispatch({
type: UPDATE_PROFILE,
payload: res.data
});
dispatch(setAlert('Education Removed', 'success'));
} catch (err) {
dispatch({
type: PROFILE_ERROR,
payload: { msg: err.response.statusText, status: err.response.status }
});
}
};
// Delete account & profile
export const deleteAccount = () => async (dispatch) => {
if (window.confirm('Are you sure? This can NOT be undone!')) {
try {
await api.delete('/profile');
dispatch({ type: CLEAR_PROFILE });
dispatch({ type: ACCOUNT_DELETED });
dispatch(setAlert('Your account has been permanently deleted'));
} catch (err) {
dispatch({
type: PROFILE_ERROR,
payload: { msg: err.response.statusText, status: err.response.status }
});
}
}
};
+25
View File
@@ -0,0 +1,25 @@
export const SET_ALERT = 'SET_ALERT';
export const REMOVE_ALERT = 'REMOVE_ALERT';
export const REGISTER_SUCCESS = 'REGISTER_SUCCESS';
export const REGISTER_FAIL = 'REGISTER_FAIL';
export const USER_LOADED = 'USER_LOADED';
export const AUTH_ERROR = 'AUTH_ERROR';
export const LOGIN_SUCCESS = 'LOGIN_SUCCESS';
export const LOGIN_FAIL = 'LOGIN_FAIL';
export const LOGOUT = 'LOGOUT';
export const GET_PROFILE = 'GET_PROFILE';
export const GET_PROFILES = 'GET_PROFILES';
export const GET_REPOS = 'GET_REPOS';
export const NO_REPOS = 'NO_REPOS';
export const UPDATE_PROFILE = 'UPDATE_PROFILE';
export const CLEAR_PROFILE = 'CLEAR_PROFILE';
export const PROFILE_ERROR = 'PROFILE_ERROR';
export const ACCOUNT_DELETED = 'ACCOUNT_DELETED';
export const GET_POSTS = 'GET_POSTS';
export const GET_POST = 'GET_POST';
export const POST_ERROR = 'POST_ERROR';
export const UPDATE_LIKES = 'UPDATE_LIKES';
export const DELETE_POST = 'DELETE_POST';
export const ADD_POST = 'ADD_POST';
export const ADD_COMMENT = 'ADD_COMMENT';
export const REMOVE_COMMENT = 'REMOVE_COMMENT';
+71
View File
@@ -0,0 +1,71 @@
import React, { useState } from 'react';
import { Link, Navigate } from 'react-router-dom';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { login } from '../../actions/auth';
const Login = ({ login, isAuthenticated }) => {
const [formData, setFormData] = useState({
email: '',
password: ''
});
const { email, password } = formData;
const onChange = (e) =>
setFormData({ ...formData, [e.target.name]: e.target.value });
const onSubmit = (e) => {
e.preventDefault();
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={onChange}
/>
</div>
<div className="form-group">
<input
type="password"
placeholder="Password"
name="password"
value={password}
onChange={onChange}
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>
);
};
Login.propTypes = {
login: PropTypes.func.isRequired,
isAuthenticated: PropTypes.bool
};
const mapStateToProps = (state) => ({
isAuthenticated: state.auth.isAuthenticated
});
export default connect(mapStateToProps, { login })(Login);
+100
View File
@@ -0,0 +1,100 @@
import React, { useState } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { Link, Navigate } from 'react-router-dom';
import { setAlert } from '../../actions/alert';
import { register } from '../../actions/auth';
const Register = ({ setAlert, register, isAuthenticated }) => {
const [formData, setFormData] = useState({
name: '',
email: '',
password: '',
password2: ''
});
const { name, email, password, password2 } = formData;
const onChange = (e) =>
setFormData({ ...formData, [e.target.name]: e.target.value });
const onSubmit = async (e) => {
e.preventDefault();
if (password !== password2) {
setAlert('Passwords do not match', 'danger');
} else {
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={onChange}
/>
</div>
<div className="form-group">
<input
type="email"
placeholder="Email Address"
name="email"
value={email}
onChange={onChange}
/>
<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={onChange}
/>
</div>
<div className="form-group">
<input
type="password"
placeholder="Confirm Password"
name="password2"
value={password2}
onChange={onChange}
/>
</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>
);
};
Register.propTypes = {
setAlert: PropTypes.func.isRequired,
register: PropTypes.func.isRequired,
isAuthenticated: PropTypes.bool
};
const mapStateToProps = (state) => ({
isAuthenticated: state.auth.isAuthenticated
});
export default connect(mapStateToProps, { setAlert, register })(Register);
@@ -0,0 +1,64 @@
import React, { useEffect } from "react";
import { Link } from "react-router-dom";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import DashboardActions from "./DashboardActions";
import Experience from "./Experience";
import Education from "./Education";
import { getCurrentProfile, deleteAccount } from "../../actions/profile";
const Dashboard = ({
getCurrentProfile,
deleteAccount,
auth: { user },
profile: { profile },
}) => {
useEffect(() => {
getCurrentProfile();
}, [getCurrentProfile]);
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={() => deleteAccount()}>
<i className="fas fa-user-minus" /> 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>
);
};
Dashboard.propTypes = {
getCurrentProfile: PropTypes.func.isRequired,
deleteAccount: PropTypes.func.isRequired,
auth: PropTypes.object.isRequired,
profile: PropTypes.object.isRequired,
};
const mapStateToProps = (state) => ({
auth: state.auth,
profile: state.profile,
});
export default connect(mapStateToProps, { getCurrentProfile, deleteAccount })(
Dashboard
);
@@ -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;
@@ -0,0 +1,49 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { deleteEducation } from '../../actions/profile';
import formatDate from '../../utils/formatDate';
const Education = ({ education, deleteEducation }) => {
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={() => 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>
);
};
Education.propTypes = {
education: PropTypes.array.isRequired,
deleteEducation: PropTypes.func.isRequired
};
export default connect(null, { deleteEducation })(Education);
@@ -0,0 +1,50 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { deleteExperience } from '../../actions/profile';
import formatDate from '../../utils/formatDate';
const Experience = ({ experience, deleteExperience }) => {
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={() => 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>
);
};
Experience.propTypes = {
experience: PropTypes.array.isRequired,
deleteExperience: PropTypes.func.isRequired
};
export default connect(null, { deleteExperience })(Experience);
+23
View File
@@ -0,0 +1,23 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
const Alert = ({ alerts }) => (
<div className="alert-wrapper">
{alerts.map((alert) => (
<div key={alert.id} className={`alert alert-${alert.alertType}`}>
{alert.msg}
</div>
))}
</div>
);
Alert.propTypes = {
alerts: PropTypes.array.isRequired
};
const mapStateToProps = (state) => ({
alerts: state.alert
});
export default connect(mapStateToProps)(Alert);
+42
View File
@@ -0,0 +1,42 @@
import React from 'react';
import { Link, Navigate } from 'react-router-dom';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
const Landing = ({ 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>
);
};
Landing.propTypes = {
isAuthenticated: PropTypes.bool
};
const mapStateToProps = state => ({
isAuthenticated: state.auth.isAuthenticated
});
export default connect(mapStateToProps)(Landing);
+66
View File
@@ -0,0 +1,66 @@
import React, { Fragment } from 'react';
import { Link } from 'react-router-dom';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { logout } from '../../actions/auth';
const Navbar = ({ auth: { isAuthenticated }, logout }) => {
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">Dashboard</span>
</Link>
</li>
<li>
<a onClick={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="/">
<i className="fas fa-code" /> DevConnector
</Link>
</h1>
<Fragment>{isAuthenticated ? authLinks : guestLinks}</Fragment>
</nav>
);
};
Navbar.propTypes = {
logout: PropTypes.func.isRequired,
auth: PropTypes.object.isRequired
};
const mapStateToProps = (state) => ({
auth: state.auth
});
export default connect(mapStateToProps, { logout })(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;
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

+44
View File
@@ -0,0 +1,44 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { addComment } from '../../actions/post';
const CommentForm = ({ postId, addComment }) => {
const [text, setText] = useState('');
return (
<div className='post-form'>
<div className='bg-primary p'>
<h3>Leave a Comment</h3>
</div>
<form
className='form my-1'
onSubmit={e => {
e.preventDefault();
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>
);
};
CommentForm.propTypes = {
addComment: PropTypes.func.isRequired
};
export default connect(
null,
{ addComment }
)(CommentForm);
+48
View File
@@ -0,0 +1,48 @@
import React from 'react';
import { Link } from 'react-router-dom';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import formatDate from '../../utils/formatDate';
import { deleteComment } from '../../actions/post';
const CommentItem = ({
postId,
comment: { _id, text, name, avatar, user, date },
auth,
deleteComment
}) => (
<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 && user === auth.user._id && (
<button
onClick={() => deleteComment(postId, _id)}
type="button"
className="btn btn-danger"
>
<i className="fas fa-times" />
</button>
)}
</div>
</div>
);
CommentItem.propTypes = {
postId: PropTypes.string.isRequired,
comment: PropTypes.object.isRequired,
auth: PropTypes.object.isRequired,
deleteComment: PropTypes.func.isRequired
};
const mapStateToProps = (state) => ({
auth: state.auth
});
export default connect(mapStateToProps, { deleteComment })(CommentItem);
+44
View File
@@ -0,0 +1,44 @@
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import { Link, useParams } from 'react-router-dom';
import { connect } from 'react-redux';
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';
const Post = ({ getPost, post: { post, loading } }) => {
const { id } = useParams();
useEffect(() => {
getPost(id);
}, [getPost, id]);
return loading || post === null ? (
<Spinner />
) : (
<section className="container">
<Link to="/posts" className="btn">
Back To Posts
</Link>
<PostItem post={post} showActions={false} />
<CommentForm postId={post._id} />
<div className="comments">
{post.comments.map((comment) => (
<CommentItem key={comment._id} comment={comment} postId={post._id} />
))}
</div>
</section>
);
};
Post.propTypes = {
getPost: PropTypes.func.isRequired,
post: PropTypes.object.isRequired
};
const mapStateToProps = (state) => ({
post: state.post
});
export default connect(mapStateToProps, { getPost })(Post);
+44
View File
@@ -0,0 +1,44 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { addPost } from '../../actions/post';
const PostForm = ({ addPost }) => {
const [text, setText] = useState('');
return (
<div className='post-form'>
<div className='bg-primary p'>
<h3>Say Something...</h3>
</div>
<form
className='form my-1'
onSubmit={e => {
e.preventDefault();
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>
);
};
PostForm.propTypes = {
addPost: PropTypes.func.isRequired
};
export default connect(
null,
{ addPost }
)(PostForm);
+84
View File
@@ -0,0 +1,84 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
import formatDate from '../../utils/formatDate';
import { connect } from 'react-redux';
import { addLike, removeLike, deletePost } from '../../actions/post';
const PostItem = ({
addLike,
removeLike,
deletePost,
auth,
post: { _id, text, name, avatar, user, likes, comments, date },
showActions
}) => (
<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>
{showActions && (
<Fragment>
<button
onClick={() => 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={() => 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 && user === auth.user._id && (
<button
onClick={() => deletePost(_id)}
type="button"
className="btn btn-danger"
>
<i className="fas fa-times" />
</button>
)}
</Fragment>
)}
</div>
</div>
);
PostItem.defaultProps = {
showActions: true
};
PostItem.propTypes = {
post: PropTypes.object.isRequired,
auth: PropTypes.object.isRequired,
addLike: PropTypes.func.isRequired,
removeLike: PropTypes.func.isRequired,
deletePost: PropTypes.func.isRequired,
showActions: PropTypes.bool
};
const mapStateToProps = (state) => ({
auth: state.auth
});
export default connect(mapStateToProps, { addLike, removeLike, deletePost })(
PostItem
);
+38
View File
@@ -0,0 +1,38 @@
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import PostItem from './PostItem';
import PostForm from './PostForm';
import { getPosts } from '../../actions/post';
const Posts = ({ getPosts, post: { posts } }) => {
useEffect(() => {
getPosts();
}, [getPosts]);
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>
);
};
Posts.propTypes = {
getPosts: PropTypes.func.isRequired,
post: PropTypes.object.isRequired
};
const mapStateToProps = (state) => ({
post: state.post
});
export default connect(mapStateToProps, { getPosts })(Posts);
@@ -0,0 +1,118 @@
import React, { useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { addEducation } from '../../actions/profile';
const AddEducation = ({ addEducation }) => {
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 = (e) =>
setFormData({ ...formData, [e.target.name]: e.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={(e) => {
e.preventDefault();
addEducation(formData, navigate);
}}
>
<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}
value={current}
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>
);
};
AddEducation.propTypes = {
addEducation: PropTypes.func.isRequired
};
export default connect(null, { addEducation })(AddEducation);
@@ -0,0 +1,119 @@
import React, { useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { addExperience } from '../../actions/profile';
const AddExperience = ({ addExperience }) => {
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) =>
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={(e) => {
e.preventDefault();
addExperience(formData, navigate);
}}
>
<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}
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>
);
};
AddExperience.propTypes = {
addExperience: PropTypes.func.isRequired
};
export default connect(null, { addExperience })(AddExperience);
@@ -0,0 +1,276 @@
import React, { Fragment, useState, useEffect } from 'react';
import { Link, useMatch, useNavigate } from 'react-router-dom';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { createProfile, getCurrentProfile } from '../../actions/profile';
/*
NOTE: declare initialState outside of component
so that it doesn't trigger a useEffect
we can then safely use this to construct our profileData
*/
const initialState = {
company: '',
website: '',
location: '',
status: '',
skills: '',
githubusername: '',
bio: '',
twitter: '',
facebook: '',
linkedin: '',
youtube: '',
instagram: ''
};
const ProfileForm = ({
profile: { profile, loading },
createProfile,
getCurrentProfile
}) => {
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
if (!profile) getCurrentProfile();
// if we finished loading and we do have a profile
// then build our profileData
if (!loading && profile) {
const profileData = { ...initialState };
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, getCurrentProfile, profile]);
const {
company,
website,
location,
status,
skills,
githubusername,
bio,
twitter,
facebook,
linkedin,
youtube,
instagram
} = formData;
const onChange = (e) =>
setFormData({ ...formData, [e.target.name]: e.target.value });
const onSubmit = (e) => {
e.preventDefault();
createProfile(formData, navigate, profile ? true : false);
};
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>
);
};
ProfileForm.propTypes = {
createProfile: PropTypes.func.isRequired,
getCurrentProfile: PropTypes.func.isRequired,
profile: PropTypes.object.isRequired
};
const mapStateToProps = (state) => ({
profile: state.profile
});
export default connect(mapStateToProps, { createProfile, getCurrentProfile })(
ProfileForm
);
+89
View File
@@ -0,0 +1,89 @@
import React, { Fragment, useEffect } from 'react';
import PropTypes from 'prop-types';
import { Link, useParams } from 'react-router-dom';
import { connect } from 'react-redux';
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';
const Profile = ({ getProfileById, profile: { profile }, auth }) => {
const { id } = useParams();
useEffect(() => {
getProfileById(id);
}, [getProfileById, 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._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) => (
<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) => (
<ProfileEducation
key={education._id}
education={education}
/>
))}
</Fragment>
) : (
<h4>No education credentials</h4>
)}
</div>
{profile.githubusername && (
<ProfileGithub username={profile.githubusername} />
)}
</div>
</Fragment>
)}
</section>
);
};
Profile.propTypes = {
getProfileById: PropTypes.func.isRequired,
profile: PropTypes.object.isRequired,
auth: PropTypes.object.isRequired
};
const mapStateToProps = (state) => ({
profile: state.profile,
auth: state.auth
});
export default connect(mapStateToProps, { getProfileById })(Profile);
@@ -0,0 +1,34 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
const ProfileAbout = ({
profile: {
bio,
skills,
user: { name }
}
}) => (
<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>
);
ProfileAbout.propTypes = {
profile: PropTypes.object.isRequired
};
export default ProfileAbout;
@@ -0,0 +1,29 @@
import React from 'react';
import PropTypes from 'prop-types';
import formatDate from '../../utils/formatDate';
const ProfileEducation = ({
education: { school, degree, fieldofstudy, current, to, from, description }
}) => (
<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>
);
ProfileEducation.propTypes = {
education: PropTypes.object.isRequired
};
export default ProfileEducation;
@@ -0,0 +1,29 @@
import React from 'react';
import PropTypes from 'prop-types';
import formatDate from '../../utils/formatDate';
const ProfileExperience = ({
experience: { company, title, location, current, to, from, description }
}) => (
<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>
);
ProfileExperience.propTypes = {
experience: PropTypes.object.isRequired
};
export default ProfileExperience;
@@ -0,0 +1,51 @@
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { getGithubRepos } from '../../actions/profile';
const ProfileGithub = ({ username, getGithubRepos, repos }) => {
useEffect(() => {
getGithubRepos(username);
}, [getGithubRepos, 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>
);
};
ProfileGithub.propTypes = {
getGithubRepos: PropTypes.func.isRequired,
repos: PropTypes.array.isRequired,
username: PropTypes.string.isRequired
};
const mapStateToProps = state => ({
repos: state.profile.repos
});
export default connect(mapStateToProps, { getGithubRepos })(ProfileGithub);
@@ -0,0 +1,51 @@
import React from 'react';
import PropTypes from 'prop-types';
const ProfileTop = ({
profile: {
status,
company,
location,
website,
social,
user: { name, avatar }
}
}) => {
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>
);
};
ProfileTop.propTypes = {
profile: PropTypes.object.isRequired
};
export default ProfileTop;
@@ -0,0 +1,42 @@
import React from 'react';
import { Link } from 'react-router-dom';
import PropTypes from 'prop-types';
const ProfileItem = ({
profile: {
user: { _id, name, avatar },
status,
company,
location,
skills
}
}) => {
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>
);
};
ProfileItem.propTypes = {
profile: PropTypes.object.isRequired
};
export default ProfileItem;
@@ -0,0 +1,42 @@
import React from 'react';
import { Link } from 'react-router-dom';
import PropTypes from 'prop-types';
const ProfileItem = ({
profile: {
user: { _id, name, avatar },
status,
company,
location,
skills
}
}) => {
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>
);
};
ProfileItem.propTypes = {
profile: PropTypes.object.isRequired
};
export default ProfileItem;
@@ -0,0 +1,25 @@
import React from 'react';
import { Navigate } from 'react-router-dom';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import Spinner from '../layout/Spinner';
const PrivateRoute = ({
component: Component,
auth: { isAuthenticated, loading }
}) => {
if (loading) return <Spinner />;
if (isAuthenticated) return <Component />;
return <Navigate to="/login" />;
};
PrivateRoute.propTypes = {
auth: PropTypes.object.isRequired
};
const mapStateToProps = (state) => ({
auth: state.auth
});
export default connect(mapStateToProps)(PrivateRoute);
Binary file not shown.

After

Width:  |  Height:  |  Size: 292 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 MiB

+5
View File
@@ -0,0 +1,5 @@
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
+18
View File
@@ -0,0 +1,18 @@
import { SET_ALERT, REMOVE_ALERT } from '../actions/types';
const initialState = [];
function alertReducer(state = initialState, action) {
const { type, payload } = action;
switch (type) {
case SET_ALERT:
return [...state, payload];
case REMOVE_ALERT:
return state.filter((alert) => alert.id !== payload);
default:
return state;
}
}
export default alertReducer;
+53
View File
@@ -0,0 +1,53 @@
import {
REGISTER_SUCCESS,
//REGISTER_FAIL,
USER_LOADED,
AUTH_ERROR,
LOGIN_SUCCESS,
//LOGIN_FAIL,
LOGOUT,
ACCOUNT_DELETED
} from '../actions/types';
const initialState = {
token: localStorage.getItem('token'),
isAuthenticated: null,
loading: true,
user: null
};
function authReducer(state = initialState, action) {
const { type, payload } = action;
switch (type) {
case USER_LOADED:
return {
...state,
isAuthenticated: true,
loading: false,
user: payload
};
case REGISTER_SUCCESS:
case LOGIN_SUCCESS:
return {
...state,
...payload,
isAuthenticated: true,
loading: false
};
case ACCOUNT_DELETED:
case AUTH_ERROR:
case LOGOUT:
return {
...state,
token: null,
isAuthenticated: false,
loading: false,
user: null
};
default:
return state;
}
}
export default authReducer;
+12
View File
@@ -0,0 +1,12 @@
import { combineReducers } from 'redux';
import alert from './alert';
import auth from './auth';
import profile from './profile';
import post from './post';
export default combineReducers({
alert,
auth,
profile,
post
});
+83
View File
@@ -0,0 +1,83 @@
import {
GET_POSTS,
POST_ERROR,
UPDATE_LIKES,
DELETE_POST,
ADD_POST,
GET_POST,
ADD_COMMENT,
REMOVE_COMMENT
} from '../actions/types';
const initialState = {
posts: [],
post: null,
loading: true,
error: {}
};
function postReducer(state = initialState, action) {
const { type, payload } = action;
switch (type) {
case GET_POSTS:
return {
...state,
posts: payload,
loading: false
};
case GET_POST:
return {
...state,
post: payload,
loading: false
};
case ADD_POST:
return {
...state,
posts: [payload, ...state.posts],
loading: false
};
case DELETE_POST:
return {
...state,
posts: state.posts.filter((post) => post._id !== payload),
loading: false
};
case POST_ERROR:
return {
...state,
error: payload,
loading: false
};
case UPDATE_LIKES:
return {
...state,
posts: state.posts.map((post) =>
post._id === payload.id ? { ...post, likes: payload.likes } : post
),
loading: false
};
case ADD_COMMENT:
return {
...state,
post: { ...state.post, comments: payload },
loading: false
};
case REMOVE_COMMENT:
return {
...state,
post: {
...state.post,
comments: state.post.comments.filter(
(comment) => comment._id !== payload
)
},
loading: false
};
default:
return state;
}
}
export default postReducer;
+65
View File
@@ -0,0 +1,65 @@
import {
GET_PROFILE,
PROFILE_ERROR,
CLEAR_PROFILE,
UPDATE_PROFILE,
GET_PROFILES,
GET_REPOS,
NO_REPOS,
} from "../actions/types";
const initialState = {
profile: null,
profiles: [],
repos: [],
loading: true,
error: {},
};
function profileReducer(state = initialState, action) {
const { type, payload } = action;
switch (type) {
case GET_PROFILE:
case UPDATE_PROFILE:
return {
...state,
profile: payload,
loading: false,
};
case GET_PROFILES:
return {
...state,
profiles: payload,
loading: false,
};
case PROFILE_ERROR:
return {
...state,
error: payload,
loading: false,
profile: null,
};
case CLEAR_PROFILE:
return {
...state,
profile: null,
repos: [],
};
case GET_REPOS:
return {
...state,
repos: payload,
loading: false,
};
case NO_REPOS:
return {
...state,
repos: [],
};
default:
return state;
}
}
export default profileReducer;
+39
View File
@@ -0,0 +1,39 @@
import { createStore, applyMiddleware } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension';
import thunk from 'redux-thunk';
import rootReducer from './reducers';
import setAuthToken from './utils/setAuthToken';
const initialState = {};
const middleware = [thunk];
const store = createStore(
rootReducer,
initialState,
composeWithDevTools(applyMiddleware(...middleware))
);
/*
NOTE: set up a store subscription listener
to store the users token in localStorage
*/
/*
initialize current state from redux store for subscription comparison
preventing undefined error
*/
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;
setAuthToken(token);
}
});
export default store;
+22
View File
@@ -0,0 +1,22 @@
import axios from 'axios';
import store from '../store';
import { LOGOUT } from '../actions/types';
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: LOGOUT });
}
return Promise.reject(err);
}
);
export default api;
+6
View File
@@ -0,0 +1,6 @@
function formatDate(date) {
return new Intl.DateTimeFormat().format(new Date(date));
}
export default formatDate;
+13
View File
@@ -0,0 +1,13 @@
import axios from "axios";
const setAuthToken = (token) => {
if (token) {
axios.defaults.headers.common["x-auth-token"] = token;
localStorage.setItem(('token'), token)
} else {
delete axios.defaults.common["x-auth-token"];
localStorage.removeItem('token')
}
};
export default setAuthToken;