project done
This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
node_modules
|
||||||
|
default.json
|
||||||
|
.vscode
|
||||||
Generated
+28706
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"name": "client",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@testing-library/jest-dom": "^5.16.5",
|
||||||
|
"@testing-library/react": "^13.4.0",
|
||||||
|
"@testing-library/user-event": "^13.5.0",
|
||||||
|
"axios": "^1.1.3",
|
||||||
|
"moment": "^2.29.4",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-moment": "^1.1.2",
|
||||||
|
"react-redux": "^8.0.4",
|
||||||
|
"react-router-dom": "^6.4.2",
|
||||||
|
"react-scripts": "5.0.1",
|
||||||
|
"redux": "^4.2.0",
|
||||||
|
"redux-devtools-extension": "^2.13.9",
|
||||||
|
"redux-thunk": "^2.4.1",
|
||||||
|
"web-vitals": "^2.1.4"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "react-scripts start",
|
||||||
|
"build": "react-scripts build",
|
||||||
|
"test": "react-scripts test",
|
||||||
|
"eject": "react-scripts eject"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"extends": [
|
||||||
|
"react-app",
|
||||||
|
"react-app/jest"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"browserslist": {
|
||||||
|
"production": [
|
||||||
|
">0.2%",
|
||||||
|
"not dead",
|
||||||
|
"not op_mini all"
|
||||||
|
],
|
||||||
|
"development": [
|
||||||
|
"last 1 chrome version",
|
||||||
|
"last 1 firefox version",
|
||||||
|
"last 1 safari version"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"proxy":"http://localhost:5000/"
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 3.8 KiB |
@@ -0,0 +1,21 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="theme-color" content="#000000" />
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://use.fontawesome.com/releases/v5.8.1/css/all.css"
|
||||||
|
integrity="sha384-50oBUHEmvpQ+1lW4y57PTFmhCaXp0ML5d60M1M7uH2+nqUivzIebhndOJK28anvf"
|
||||||
|
crossorigin="anonymous"
|
||||||
|
/>
|
||||||
|
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||||
|
<title>Welcome To DevConnector</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<div id="root"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"short_name": "React App",
|
||||||
|
"name": "Create React App Sample",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "favicon.ico",
|
||||||
|
"sizes": "64x64 32x32 24x24 16x16",
|
||||||
|
"type": "image/x-icon"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"start_url": ".",
|
||||||
|
"display": "standalone",
|
||||||
|
"theme_color": "#000000",
|
||||||
|
"background_color": "#ffffff"
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
@@ -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);
|
||||||
|
};
|
||||||
@@ -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 });
|
||||||
@@ -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 }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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 }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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';
|
||||||
@@ -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);
|
||||||
@@ -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);
|
||||||
@@ -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);
|
||||||
@@ -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);
|
||||||
@@ -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);
|
||||||
@@ -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;
|
||||||
@@ -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 |
@@ -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);
|
||||||
@@ -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);
|
||||||
@@ -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);
|
||||||
@@ -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);
|
||||||
@@ -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
|
||||||
|
);
|
||||||
@@ -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
|
||||||
|
);
|
||||||
@@ -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 |
@@ -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 />);
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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
|
||||||
|
});
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
function formatDate(date) {
|
||||||
|
return new Intl.DateTimeFormat().format(new Date(date));
|
||||||
|
}
|
||||||
|
|
||||||
|
export default formatDate;
|
||||||
|
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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' });
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
@@ -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);
|
||||||
@@ -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);
|
||||||
@@ -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);
|
||||||
Generated
+4076
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"name": "mern-stack-front-to-back",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "server.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node server",
|
||||||
|
"server": "nodemon server",
|
||||||
|
"client": "npm start --prefix client --trace-depracation",
|
||||||
|
"dev": "concurrently \"npm run server\" \"npm run client\""
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.1.3",
|
||||||
|
"bcryptjs": "^2.4.3",
|
||||||
|
"config": "^3.3.8",
|
||||||
|
"express": "^4.18.2",
|
||||||
|
"express-validator": "^6.14.2",
|
||||||
|
"gravatar": "^1.8.2",
|
||||||
|
"jsonwebtoken": "^8.5.1",
|
||||||
|
"mongoose": "^6.6.5",
|
||||||
|
"normalize-url": "^5.0.0",
|
||||||
|
"request": "^2.88.2",
|
||||||
|
"uuid": "^9.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"concurrently": "^7.4.0",
|
||||||
|
"nodemon": "^2.0.20"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
const express = require("express");
|
||||||
|
const router = express.Router();
|
||||||
|
const gravatar = require("gravatar");
|
||||||
|
const bcrypt = require("bcryptjs");
|
||||||
|
const jwt = require("jsonwebtoken");
|
||||||
|
const config = require("config");
|
||||||
|
const { check, validationResult } = require("express-validator");
|
||||||
|
const User = require("../../models/User");
|
||||||
|
const normalize = require('normalize-url');
|
||||||
|
|
||||||
|
// @route POST api/users
|
||||||
|
// @desc Register user
|
||||||
|
// @access Public
|
||||||
|
router.post(
|
||||||
|
"/",
|
||||||
|
check("name", "Name is required").notEmpty(),
|
||||||
|
check("email", "Please include a valid email").isEmail(),
|
||||||
|
check(
|
||||||
|
"password",
|
||||||
|
"Please enter a password with 6 or more characters"
|
||||||
|
).isLength({ min: 6 }),
|
||||||
|
async (req, res) => {
|
||||||
|
const errors = validationResult(req);
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
return res.status(400).json({ errors: errors.array() });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { name, email, password } = req.body;
|
||||||
|
|
||||||
|
try {
|
||||||
|
let user = await User.findOne({ email });
|
||||||
|
|
||||||
|
if (user) {
|
||||||
|
return res
|
||||||
|
.status(400)
|
||||||
|
.json({ errors: [{ msg: "User already exists" }] });
|
||||||
|
}
|
||||||
|
|
||||||
|
const avatar = normalize(
|
||||||
|
gravatar.url(email, {
|
||||||
|
s: "200",
|
||||||
|
r: "pg",
|
||||||
|
d: "mm",
|
||||||
|
}),
|
||||||
|
{ forceHttps: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
user = new User({
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
avatar,
|
||||||
|
password,
|
||||||
|
});
|
||||||
|
|
||||||
|
const salt = await bcrypt.genSalt(10);
|
||||||
|
|
||||||
|
user.password = await bcrypt.hash(password, salt);
|
||||||
|
|
||||||
|
await user.save();
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
user: {
|
||||||
|
id: user.id,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
jwt.sign(
|
||||||
|
payload,
|
||||||
|
config.get("jwtSecret"),
|
||||||
|
{ expiresIn: "5 days" },
|
||||||
|
(err, token) => {
|
||||||
|
if (err) throw err;
|
||||||
|
res.json({ token });
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err.message);
|
||||||
|
res.status(500).send("Server error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const connectDB= require('./config/db')
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
connectDB();
|
||||||
|
|
||||||
|
app.use(express.json({extended:false}));
|
||||||
|
app.get('/',(req,res)=> res.send('API Running'));
|
||||||
|
|
||||||
|
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'))
|
||||||
|
const PORT = process.env.PORT || 5000;
|
||||||
|
|
||||||
|
app.listen(PORT,()=> console.log(`Server started on port ${PORT}`));
|
||||||
Reference in New Issue
Block a user