Compare commits

..

10 Commits

Author SHA1 Message Date
QkoSad 97ff4a5263 grammar 2025-07-01 20:54:31 +03:00
QkoSad 3ba8c2abd5 fixed problems with docker that only existed on other systems 2025-06-17 16:53:54 +03:00
QkoSad 5a2b291bdd no idea what i did 2025-06-16 17:44:13 +03:00
Andrean 54da6e1bf5 Update README.md 2024-09-30 18:28:14 +00:00
QkoSad 27c1741bc0 fixed a docker issue updated the README 2024-09-30 21:26:58 +03:00
QkoSad da43bec842 Removed all of the duplicate js files from the rebase for github 2024-09-29 00:11:08 +03:00
QkoSad 1b88aa7e56 removed duplicate files 2024-09-29 00:00:58 +03:00
QkoSad 04b51ae321 minor change 2024-09-29 00:00:52 +03:00
QkoSad bcbffad32d fixed website breaking the server 2024-09-29 00:00:52 +03:00
QkoSad 46f6280152 Fix an issue with profile form 2024-09-29 00:00:52 +03:00
73 changed files with 142 additions and 2930 deletions
+4 -1
View File
@@ -1,3 +1,6 @@
node_modules node_modules
App.css
start_in_tmux.sh start_in_tmux.sh
default.json
production.json
bin
obj
+19
View File
@@ -0,0 +1,19 @@
# Dev connector
---
> Web app created with React, Redux, MUI in the front end. Express and mongoose
> on the back end. It also has docker images.
## Docker Usage:
`docker compose -f docker-compose.yml up`
> The app is available at http://localhost:8080
## Run the app locally
Requires running mongodb service.
In the client directory:
`npm run dev`
In the server directory:
```MONGO_URL=mongodb://the_username:the_password@localhost:3456/the_database JWT_SECRET=mysecrettoken npm run server```
The app will be available at http://localhost:5173
-23
View File
@@ -1,23 +0,0 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
-2
View File
@@ -1,5 +1,3 @@
# The first FROM is now a stage called build-stage
FROM node:20 AS build-stage FROM node:20 AS build-stage
WORKDIR /usr/src/app WORKDIR /usr/src/app
-11
View File
@@ -1,11 +0,0 @@
services:
app:
image: devcon-frontend-dev
build:
context: . # The context will pick this directory as the "build context"
dockerfile: dev.Dockerfile # This will simply tell which dockerfile to read
volumes:
- ./:/usr/src/app # The path can be relative, so ./ is enough to say "the same location as the docker-compose.yml"
ports:
- 5173:3000
container_name: devcon-front-dev
-11
View File
@@ -1,11 +0,0 @@
services:
app:
image: todo-front-dev
build:
context: . # The context will pick this directory as the "build context"
dockerfile: dev.Dockerfile # This will simply tell which dockerfile to read
volumes:
- ./:/usr/src/app # The path can be relative, so ./ is enough to say "the same location as the docker-compose.yml"
ports:
- 5173:5173
container_name: todo-front
+15 -8
View File
@@ -13,7 +13,7 @@
"@mui/icons-material": "^6.1.1", "@mui/icons-material": "^6.1.1",
"@mui/material": "^6.1.1", "@mui/material": "^6.1.1",
"@reduxjs/toolkit": "^1.9.5", "@reduxjs/toolkit": "^1.9.5",
"@testing-library/jest-dom": "^5.16.5", "@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0", "@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0", "@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.5.2", "@types/jest": "^27.5.2",
@@ -142,7 +142,7 @@
"@babel/helper-validator-option": "^7.24.8", "@babel/helper-validator-option": "^7.24.8",
"browserslist": "^4.23.1", "browserslist": "^4.23.1",
"lru-cache": "^5.1.1", "lru-cache": "^5.1.1",
"semver": "^6.3.0" "semver": "^6.3.1"
}, },
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
@@ -175,6 +175,9 @@
}, },
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
},
"peerDependencies": {
"@babel/core": "^7.0.0"
} }
}, },
"node_modules/@babel/helper-plugin-utils": { "node_modules/@babel/helper-plugin-utils": {
@@ -1656,10 +1659,10 @@
"@babel/code-frame": "^7.10.4", "@babel/code-frame": "^7.10.4",
"@babel/runtime": "^7.12.5", "@babel/runtime": "^7.12.5",
"@types/aria-query": "^5.0.1", "@types/aria-query": "^5.0.1",
"aria-query": "^5.0.0", "aria-query": "5.1.3",
"chalk": "^4.1.0", "chalk": "^4.1.0",
"dom-accessibility-api": "^0.5.9", "dom-accessibility-api": "^0.5.9",
"lz-string": "^1.4.4", "lz-string": "^1.5.0",
"pretty-format": "^27.0.2" "pretty-format": "^27.0.2"
}, },
"engines": { "engines": {
@@ -1809,7 +1812,7 @@
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/types": "^7.3.0" "@babel/types": "^7.20.7"
} }
}, },
"node_modules/@types/estree": { "node_modules/@types/estree": {
@@ -1835,8 +1838,8 @@
"integrity": "sha512-mpT8LJJ4CMeeahobofYWIjFo0xonRS/HfxnVEPMPFSQdGUt1uHCnoPT7Zhb+sjDU2wz0oKV0OLUR0WzrHNgfeA==", "integrity": "sha512-mpT8LJJ4CMeeahobofYWIjFo0xonRS/HfxnVEPMPFSQdGUt1uHCnoPT7Zhb+sjDU2wz0oKV0OLUR0WzrHNgfeA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"expect": "^29.0.0", "jest-matcher-utils": "^27.0.0",
"pretty-format": "^29.0.0" "pretty-format": "^27.0.0"
} }
}, },
"node_modules/@types/parse-json": { "node_modules/@types/parse-json": {
@@ -2027,6 +2030,10 @@
{ {
"type": "tidelift", "type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/browserslist" "url": "https://tidelift.com/funding/github/npm/browserslist"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
} }
], ],
"license": "MIT", "license": "MIT",
@@ -3527,7 +3534,7 @@
"react": "^16.8 || ^17.0 || ^18.0", "react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0",
"react-native": ">=0.59", "react-native": ">=0.59",
"redux": "^4" "redux": "^4 || ^5.0.0-beta.0"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
"@types/react": { "@types/react": {
-99
View File
@@ -1,99 +0,0 @@
import React, { useEffect } from 'react';
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
import Navbar from './components/layout/Navbar';
import Landing from './components/layout/Landing';
import Register from './components/auth/Register';
import Login from './components/auth/Login';
import Alert from './components/layout/Alert';
import Dashboard from './components/dashboard/Dashboard';
import ProfileForm from './components/profile-forms/ProfileForm';
import AddExperience from './components/profile-forms/AddExperience';
import AddEducation from './components/profile-forms/AddEducation';
import Profiles from './components/profiles/Profiles';
import Profile from './components/profile/Profile';
import Posts from './components/posts/Posts';
import Post from './components/post/Post';
import NotFound from './components/layout/NotFound';
import PrivateRoute from './components/routing/PrivateRoute';
// Redux
import { Provider } from 'react-redux';
import store from './store';
import { loadUser } from './actions/auth';
import setAuthToken from './utils/setAuthToken';
import './App.css';
import { logOut } from './reducers/auth';
// Level - 1
//
const App = () => {
useEffect(() => {
// check for token in LS when app first runs
if (localStorage.token) {
// if there is a token set axios headers for all requests
setAuthToken(localStorage.token);
}
// try to fetch a user, if no token or invalid token we
// will get a 401 response from our API
store.dispatch(loadUser());
// log user out from all tabs if they log out in one tab
window.addEventListener("storage", () => {
if (!localStorage.token) store.dispatch(logOut);
});
}, []);
return (
<Provider store={store}>
<Router>
<Navbar />
<Alert />
<Routes>
<Route path="/" element={<Landing />} />
<Route path="register" element={<Register />} />
<Route path="login" element={<Login />} />
<Route path="profiles" element={<Profiles />} />
<Route path="profile/:id" element={<Profile />} />
<Route
path="dashboard"
element={<PrivateRoute component={Dashboard} />}
/>
<Route
path="create-profile"
element={<PrivateRoute component={ProfileForm} />}
/>
<Route
path="edit-profile"
element={<PrivateRoute component={ProfileForm} />}
/>
<Route
path="add-experience"
element={<PrivateRoute component={AddExperience} />}
/>
<Route
path="add-education"
element={<PrivateRoute component={AddEducation} />}
/>
<Route path="posts" element={<PrivateRoute component={Posts} />} />
<Route path="posts/:id" element={<PrivateRoute component={Post} />} />
<Route path="/*" element={<NotFound />} />
</Routes>
</Router>
</Provider>
);
/*
return (
<Provider store={store}>
<Router>
<Routes>
<Route path="login" element={<Login />} />
</Routes>
</Router>
</Provider>
);*/
};
export default App;
-5
View File
@@ -1,6 +1 @@
1. When creating profile in the Profile form need to be added validation for the
email because if the email is not email it gets sent to the server and crashes
Fix all the form data apis, formData
Extract the inpudate component from addeducation
maybe fix that addeducation
-9
View File
@@ -1,9 +0,0 @@
import { v4 as uuidv4 } from 'uuid';
import {removeAlert, setAlert } from '../reducers/alert';
export const createAlert = (msg, alertType, timeout = 5000) => dispatch => {
const id = uuidv4();
dispatch(setAlert({ msg, alertType, id }));
setTimeout(() => dispatch(removeAlert(id)), timeout);
};
-68
View File
@@ -1,68 +0,0 @@
import api from "../utils/api";
import { createAlert } from "./alert";
import {
loginSucces,
authError,
registerSuccess,
logOut,
userLoaded,
} from "../reducers/auth";
/*
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
*/
export const login = (email, password) => async (dispatch) => {
const body = { email, password };
try {
const res = await api.post("/auth", body);
dispatch(loginSucces(res.data));
dispatch(loadUser());
} catch (err) {
const errors = err.response.data.errors;
if (errors) {
errors.forEach((error) => dispatch(createAlert(error.msg, "danger")));
}
dispatch({
type: "auth/loginFail",
});
}
};
export const loadUser = () => async (dispatch) => {
try {
const res = await api.get("/auth");
dispatch(userLoaded(res.data));
} catch (err) {
dispatch(authError());
}
};
// Register User
export const register = (formData) => async (dispatch) => {
try {
const res = await api.post("/users", formData);
dispatch(registerSuccess(res.data));
dispatch(loadUser());
} catch (err) {
const errors = err.response.data.errors;
if (errors) {
errors.forEach((error) => dispatch(createAlert(error.msg, "danger")));
}
dispatch({
type: "auth/registerFail",
});
}
};
export const logout = () => async (dispatch) => {
dispatch(logOut);
};
-124
View File
@@ -1,124 +0,0 @@
import {
removeComment,
addCommentAction,
updateLikes,
postError,
deletePostAction,
getPostAction,
getPostsAction,
addPostAction,
} from "../reducers/post";
import api from "../utils/api";
import { createAlert } from "./alert";
// Get posts
export const getPosts = () => async (dispatch) => {
try {
const res = await api.get("/posts");
dispatch(getPostsAction(res.data));
} catch (err) {
dispatch(
postError({ 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(updateLikes({ id, likes: res.data }));
} catch (err) {
dispatch(
postError({ 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(updateLikes({ id, likes: res.data }));
} catch (err) {
dispatch(
postError({ msg: err.response.statusText, status: err.response.status })
);
}
};
// Delete post
export const deletePost = (id) => async (dispatch) => {
try {
await api.delete(`/posts/${id}`);
dispatch(deletePostAction(id));
dispatch(createAlert("Post Removed", "success"));
} catch (err) {
dispatch(
postError({ 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(addPostAction(res.data));
dispatch(createAlert("Post Created", "success"));
} catch (err) {
dispatch(
postError({ 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(getPostAction(res.data));
} catch (err) {
dispatch(
postError({ 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(addCommentAction(res.data));
dispatch(createAlert("Comment Added", "success"));
} catch (err) {
dispatch(
postError({ 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(removeComment(commentId));
dispatch(createAlert("Comment Removed", "success"));
} catch (err) {
dispatch(
postError({ msg: err.response.statusText, status: err.response.status })
);
}
};
-210
View File
@@ -1,210 +0,0 @@
import api from "../utils/api";
import { createAlert } from "./alert";
import {
noRepos,
getRepos,
clearProfile,
profileError,
getProfilesType,
updateProfile,
getProfile,
} from "../reducers/profile";
import { accountDeleted } from "../reducers/auth";
// Get current users profile
export const getCurrentProfile = () => async (dispatch) => {
try {
const res = await api.get("/profile/me");
dispatch(getProfile(res.data));
} catch (err) {
dispatch(
profileError({
msg: err.response.statusText,
status: err.response.status,
})
);
}
};
// Get all profiles
export const getProfiles = () => async (dispatch) => {
dispatch(clearProfile());
try {
const res = await api.get("/profile");
dispatch(getProfilesType(res.data));
} catch (err) {
dispatch(
profileError({
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(getProfile(res.data));
} catch (err) {
dispatch(
profileError({
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(getRepos(res.data));
} catch (err) {
dispatch(noRepos());
}
};
// Create or update profile
export const createProfile =
(formData, edit = false) =>
async (dispatch) => {
try {
const res = await api.post("/profile", formData);
dispatch(getProfile(res.data));
dispatch(
createAlert(edit ? "Profile Updated" : "Profile Created", "success")
);
} catch (err) {
const errors = err.response.data.errors;
if (errors) {
errors.forEach((error) => dispatch(createAlert(error.msg, "danger")));
}
dispatch(
profileError({
msg: err.response.statusText,
status: err.response.status,
})
);
}
};
// Add Experience
export const addExperience = (formData) => async (dispatch) => {
try {
const res = await api.put("/profile/experience", formData);
dispatch(updateProfile(res.data));
dispatch(createAlert("Experience Added", "success"));
} catch (err) {
const errors = err.response.data.errors;
if (errors) {
errors.forEach((error) => dispatch(createAlert(error.msg, "danger")));
}
dispatch(
profileError({
msg: err.response.statusText,
status: err.response.status,
})
);
}
};
// Add Education
export const addEducation = (formData) => async (dispatch) => {
try {
const res = await api.put("/profile/education", formData);
dispatch(updateProfile(res.data));
dispatch(createAlert("Education Added", "success"));
} catch (err) {
const errors = err.response.data.errors;
if (errors) {
errors.forEach((error) => dispatch(createAlert(error.msg, "danger")));
}
dispatch(
profileError({
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(updateProfile(res.data));
dispatch(createAlert("Experience Removed", "success"));
} catch (err) {
dispatch(
profileError({
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(updateProfile(res.data));
dispatch(createAlert("Education Removed", "success"));
} catch (err) {
dispatch(
profileError({
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(clearProfile());
dispatch(accountDeleted());
dispatch(createAlert("Your account has been permanently deleted"));
} catch (err) {
dispatch(
profileError({
msg: err.response.statusText,
status: err.response.status,
})
);
}
}
};
-61
View File
@@ -1,61 +0,0 @@
import React, { useState } from "react";
import { Link, Navigate } from "react-router-dom";
import { useDispatch, useSelector } from "react-redux";
import { login } from "../../actions/auth";
const Login = () => {
const [formData, setFormData] = useState({
email: "",
password: "",
});
const dispatch = useDispatch();
const { email, password } = formData;
const isAuthenticated = useSelector((state) => state.auth.isAuthenticated);
const onChange = (e) =>
setFormData({ ...formData, [e.target.name]: e.target.value });
const onSubmit = async (e) => {
e.preventDefault();
await dispatch(login(email, password));
};
if (isAuthenticated) {
return <Navigate to="/dashboard" />;
}
return (
<section className="container">
<h1 className="large text-primary">Sign In</h1>
<p className="lead">
<i className="fas fa-user" /> Sign Into Your Account
</p>
<form className="form" onSubmit={onSubmit}>
<div className="form-group">
<input
type="email"
placeholder="Email Address"
name="email"
value={email}
onChange={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>
);
};
export default Login;
-91
View File
@@ -1,91 +0,0 @@
import React, { useState } from 'react';
import { useDispatch,useSelector } from 'react-redux';
import { Link, Navigate } from 'react-router-dom';
import { createAlert } from '../../actions/alert';
import { register } from '../../actions/auth';
const Register = () => {
const dispatch = useDispatch();
const [formData, setFormData] = useState({
name: '',
email: '',
password: '',
password2: ''
});
const isAuthenticated = useSelector(state=>state.auth.isAuthenticated)
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) {
await dispatch(createAlert('Passwords do not match', 'danger'));
} else {
await dispatch(register({ name, email, password }));
}
};
if (isAuthenticated) {
return <Navigate to="/dashboard" />;
}
return (
<section className="container">
<h1 className="large text-primary">Sign Up</h1>
<p className="lead">
<i className="fas fa-user" /> Create Your Account
</p>
<form className="form" onSubmit={onSubmit}>
<div className="form-group">
<input
type="text"
placeholder="Name"
name="name"
value={name}
onChange={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>
);
};
export default Register;
-4
View File
@@ -59,7 +59,6 @@ export default function SignUp() {
name="name" name="name"
required required
fullWidth fullWidth
id="name"
label="Name" label="Name"
autoFocus autoFocus
/> />
@@ -68,7 +67,6 @@ export default function SignUp() {
<TextField <TextField
required required
fullWidth fullWidth
id="email"
label="Email Address" label="Email Address"
name="email" name="email"
/> />
@@ -80,7 +78,6 @@ export default function SignUp() {
name="password" name="password"
label="Password" label="Password"
type="password" type="password"
id="password"
/> />
</Grid> </Grid>
<Grid size={{ xs: 12 }}> <Grid size={{ xs: 12 }}>
@@ -90,7 +87,6 @@ export default function SignUp() {
name="password2" name="password2"
label="Repeat Password" label="Repeat Password"
type="password" type="password"
id="password2"
/> />
</Grid> </Grid>
</Grid> </Grid>
@@ -1,52 +0,0 @@
import React, { useEffect } from "react";
import { Link } from "react-router-dom";
import { useDispatch, useSelector } from "react-redux";
import DashboardActions from "./DashboardActions";
import Experience from "./Experience";
import Education from "./Education";
import { getCurrentProfile, deleteAccount } from "../../actions/profile";
const Dashboard = () => {
const dispatch = useDispatch();
useEffect(() => {
function fetchData() {
dispatch(getCurrentProfile());
}
fetchData();
}, [dispatch]);
const user = useSelector((state) => state.auth.user);
const profile = useSelector((state) => state.profile.profile);
return (
<section className="container">
<h1 className="large text-primary">Dashboard</h1>
<p className="lead">
<i className="fas fa-user" /> Welcome {user && user.name}
</p>
{profile !== null ? (
<>
<DashboardActions />
<Experience experience={profile.experience} />
<Education education={profile.education} />
<div className="my-2">
<button
className="btn btn-danger"
onClick={async () => await dispatch(deleteAccount())}
>
<i className="fas fa-user" /> Delete My Account
</button>
</div>
</>
) : (
<>
<p>You have not yet setup a profile, please add some info</p>
<Link to="/create-profile" className="btn btn-primary my-1">
Create Profile
</Link>
</>
)}
</section>
);
};
export default Dashboard;
@@ -1,20 +0,0 @@
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;
@@ -1,44 +0,0 @@
import React, { Fragment } from "react";
import {useDispatch } from "react-redux";
import { deleteEducation } from "../../actions/profile";
import formatDate from "../../utils/formatDate";
const Education = ({education}) => {
const dispatch = useDispatch();
const educations = education.map((edu) => (
<tr key={edu._id}>
<td>{edu.school}</td>
<td className="hide-sm">{edu.degree}</td>
<td>
{formatDate(edu.from)} - {edu.to ? formatDate(edu.to) : "Now"}
</td>
<td>
<button
onClick={async () => await dispatch(deleteEducation(edu._id))}
className="btn btn-danger"
>
Delete
</button>
</td>
</tr>
));
return (
<Fragment>
<h2 className="my-2">Education Credentials</h2>
<table className="table">
<thead>
<tr>
<th>School</th>
<th className="hide-sm">Degree</th>
<th className="hide-sm">Years</th>
<th />
</tr>
</thead>
<tbody>{educations}</tbody>
</table>
</Fragment>
);
};
export default Education;
@@ -1,46 +0,0 @@
import React, { Fragment } from 'react';
import {useDispatch } from 'react-redux';
import { deleteExperience } from '../../actions/profile';
import formatDate from '../../utils/formatDate';
const Experience = ({experience}) => {
const dispatch = useDispatch();
const experiences = experience.map((exp) => (
<tr key={exp._id}>
<td>{exp.company}</td>
<td className="hide-sm">{exp.title}</td>
<td>
{formatDate(exp.from)} - {exp.to ? formatDate(exp.to) : 'Now'}
</td>
<td>
<button
onClick={async () => dispatch(deleteExperience(exp._id))}
className="btn btn-danger"
>
Delete
</button>
</td>
</tr>
));
return (
<Fragment>
<h2 className="my-2">Experience Credentials</h2>
<table className="table">
<thead>
<tr>
<th>Company</th>
<th className="hide-sm">Title</th>
<th className="hide-sm">Years</th>
<th />
</tr>
</thead>
<tbody>{experiences}</tbody>
</table>
</Fragment>
);
};
export default Experience;
-16
View File
@@ -1,16 +0,0 @@
import React from "react";
import { useSelector } from "react-redux";
const Alert = () => {
const alerts = useSelector((state) => state.alert);
return (
<div className="alert-wrapper">
{alerts.map((alert) => (
<div key={alert.id} className={`alert alert-${alert.alertType}`}>
{alert.msg}
</div>
))}
</div>
);
};
export default Alert;
-34
View File
@@ -1,34 +0,0 @@
import React from "react";
import { Link, Navigate } from "react-router-dom";
import { useSelector } from "react-redux";
const Landing = () => {
const isAuthenticated = useSelector((state) => state.auth.isAuthenticated);
if (isAuthenticated) {
return <Navigate to="/dashboard" />;
}
return (
<section className="landing">
<div className="dark-overlay">
<div className="landing-inner">
<h1 className="x-large">Developer Connector</h1>
<p className="lead">
Create a developer profile/portfolio, share posts and get help from
other developers
</p>
<div className="buttons">
<Link to="/register" className="btn btn-primary">
Sign Up
</Link>
<Link to="/login" className="btn btn-light">
Login
</Link>
</div>
</div>
</div>
</section>
);
};
export default Landing;
-57
View File
@@ -1,57 +0,0 @@
import React, { Fragment } from "react";
import { Link } from "react-router-dom";
import { useDispatch, useSelector } from "react-redux";
import { logOut } from "../../reducers/auth";
const Navbar = () => {
const isAuthenticated = useSelector((state) => state.auth.isAuthenticated);
const dispatch = useDispatch();
const authLinks = (
<ul>
<li>
<Link to="/profiles">Developers</Link>
</li>
<li>
<Link to="/posts">Posts</Link>
</li>
<li>
<Link to="/dashboard">
<i className="fas fa-user" /> <span className="hide-sm">Profile</span>
</Link>
</li>
<li>
<a onClick={()=>dispatch(logOut())} href="#!">
<i className="fas fa-sign-out-alt" />{' '}
<span className="hide-sm">Logout</span>
</a>
</li>
</ul>
);
const guestLinks = (
<ul>
<li>
<Link to="/profiles">Developers</Link>
</li>
<li>
<Link to="/register">Register</Link>
</li>
<li>
<Link to="/login">Login</Link>
</li>
</ul>
);
return (
<nav className="navbar bg-dark">
<h1>
<Link to="/posts">
<i className="fas fa-code" /> DevConnector
</Link>
</h1>
<Fragment>{isAuthenticated ? authLinks : guestLinks}</Fragment>
</nav>
);
};
export default Navbar;
+1 -2
View File
@@ -1,4 +1,3 @@
import React from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { logOut } from "../../reducers/auth"; import { logOut } from "../../reducers/auth";
import { useAppDispatch, useAppSelector } from "../../utils/hooks"; import { useAppDispatch, useAppSelector } from "../../utils/hooks";
@@ -69,7 +68,7 @@ const Navbar = () => {
<AppBar position="static"> <AppBar position="static">
<Container maxWidth="xl"> <Container maxWidth="xl">
<Toolbar disableGutters> <Toolbar disableGutters>
{/*Icon for big dislay*/} {/*Icon for big display*/}
<AdbIcon sx={{ display: { xs: "none", md: "flex" }, mr: 1 }} /> <AdbIcon sx={{ display: { xs: "none", md: "flex" }, mr: 1 }} />
{/*LOGO link for bi display*/} {/*LOGO link for bi display*/}
<Typography <Typography
-14
View File
@@ -1,14 +0,0 @@
import React from 'react';
const NotFound = () => {
return (
<section className="container">
<h1 className="x-large text-primary">
<i className="fas fa-exclamation-triangle" /> Page Not Found
</h1>
<p className="large">Sorry, this page does not exist</p>
</section>
);
};
export default NotFound;
-14
View File
@@ -1,14 +0,0 @@
import React, { Fragment } from 'react';
import spinner from './spinner.gif';
const Spinner = () => (
<Fragment>
<img
src={spinner}
style={{ width: '200px', margin: 'auto', display: 'block' }}
alt="Loading..."
/>
</Fragment>
);
export default Spinner;
-39
View File
@@ -1,39 +0,0 @@
import React, { useState } from 'react';
import { useDispatch } from 'react-redux';
import { addComment } from '../../actions/post';
const CommentForm = ({ postId }) => {
const [text, setText] = useState('');
const dispatch = useDispatch();
return (
<div className='post-form'>
<div className='bg-primary p'>
<h3>Leave a Comment</h3>
</div>
<form
className='form my-1'
onSubmit={async(e) => {
e.preventDefault();
await dispatch(addComment(postId, { text }));
setText('');
}}
>
<textarea
name='text'
cols='30'
rows='5'
placeholder='Comment the post'
value={text}
onChange={e => setText(e.target.value)}
required
/>
<input type='submit' className='btn btn-dark my-1' value='Submit' />
</form>
</div>
);
};
export default CommentForm
-38
View File
@@ -1,38 +0,0 @@
import React from "react";
import { Link } from "react-router-dom";
import { useDispatch, useSelector } from "react-redux";
import formatDate from "../../utils/formatDate";
import { deleteComment } from "../../actions/post";
const CommentItem = ({
postId,
comment: { _id, text, name, avatar, user, date },
}) => {
const dispatch = useDispatch();
const auth = useSelector((state) => state.auth);
return (
<div className="post bg-white p-1 my-1">
<div>
<Link to={`/profile/${user}`}>
<img className="round-img" src={avatar} alt="" />
<h4>{name}</h4>
</Link>
</div>
<div>
<p className="my-1">{text}</p>
<p className="post-date">Posted on {formatDate(date)}</p>
{!auth.loading && user === auth.user._id && (
<button
onClick={async () => await dispatch(deleteComment(postId, _id))}
type="button"
className="btn btn-danger"
>
<i className="fas fa-times" />
</button>
)}
</div>
</div>
);
};
export default CommentItem;
-39
View File
@@ -1,39 +0,0 @@
import React, { useEffect } from "react";
import { Link, useParams } from "react-router-dom";
import { useDispatch, useSelector } 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 = () => {
const dispatch = useDispatch();
const { post, loading } = useSelector((state) => state.post);
const { id } = useParams();
useEffect(() => {
async function fetchData() {
await dispatch(getPost(id));
}
fetchData();
}, [dispatch, id]);
return loading || post === null ? (
<Spinner />
) : (
<section className="container">
<Link to="/posts" className="btn">
Back To Posts
</Link>
<PostItem post={post} showActions={false} />
<CommentForm postId={post._id} />
<div className="comments">
{post.comments.map((comment) => (
<CommentItem key={comment._id} comment={comment} postId={post._id} />
))}
</div>
</section>
);
};
export default Post;
-36
View File
@@ -1,36 +0,0 @@
import React, { useState } from "react";
import { useDispatch } from "react-redux";
import { addPost } from "../../actions/post";
const PostForm = () => {
const [text, setText] = useState("");
const dispatch = useDispatch();
return (
<div className="post-form">
<div className="bg-primary p">
<h3>Say Something...</h3>
</div>
<form
className="form my-1"
onSubmit={async (e) => {
e.preventDefault();
await dispatch(addPost({ text }));
setText("");
}}
>
<textarea
name="text"
cols="30"
rows="5"
placeholder="Create a post"
value={text}
onChange={(e) => setText(e.target.value)}
required
/>
<input type="submit" className="btn btn-dark my-1" value="Submit" />
</form>
</div>
);
};
export default PostForm;
-57
View File
@@ -1,57 +0,0 @@
import React from "react";
import { Link } from "react-router-dom";
import formatDate from "../../utils/formatDate";
import { useDispatch, useSelector } from "react-redux";
import { addLike, removeLike, deletePost } from "../../actions/post";
const PostItem = ({
post: { _id, text, name, avatar, user, likes, comments, date },
}) => {
const dispatch = useDispatch();
const auth = useSelector((state) => state.auth);
return (
<div className="post bg-white p-1 my-1">
<div>
<Link to={`/profile/${user}`}>
<img className="round-img" src={avatar} alt="" />
<h4>{name}</h4>
</Link>
</div>
<div>
<p className="my-1">{text}</p>
<p className="post-date">Posted on {formatDate(date)}</p>
<button
onClick={async () => await dispatch(addLike(_id))}
type="button"
className="btn btn-light"
>
<i className="fas fa-thumbs-up" />{" "}
<span>{likes.length > 0 && <span>{likes.length}</span>}</span>
</button>
<button
onClick={async () => await dispatch(removeLike(_id))}
type="button"
className="btn btn-light"
>
<i className="fas fa-thumbs-down" />
</button>
<Link to={`/posts/${_id}`} className="btn btn-primary">
Discussion{" "}
{comments.length > 0 && (
<span className="comment-count">{comments.length}</span>
)}
</Link>
{!auth.loading && user === auth.user._id && (
<button
onClick={async () => await dispatch(deletePost(_id))}
type="button"
className="btn btn-danger"
>
<i className="fas fa-times" />
</button>
)}
</div>
</div>
);
};
export default PostItem;
-34
View File
@@ -1,34 +0,0 @@
import React, { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import PostItem from "./PostItem";
import PostForm from "./PostForm";
import { getPosts } from "../../actions/post";
const Posts = () => {
const dispatch = useDispatch();
useEffect(() => {
async function fetchData() {
await dispatch(getPosts());
}
fetchData();
}, [dispatch]);
const posts = useSelector((state) => state.post.posts);
return (
<section className="container">
<h1 className="large text-primary">Posts</h1>
<p className="lead">
<i className="fas fa-user" /> Welcome to the community
</p>
<PostForm />
<div className="posts">
{posts.map((post) => (
<PostItem key={post._id} post={post} />
))}
</div>
</section>
);
};
export default Posts;
@@ -1,116 +0,0 @@
import React, { useState } from "react";
import { Link, useNavigate } from "react-router-dom";
import { useDispatch } from "react-redux";
import { addEducation } from "../../actions/profile";
const AddEducation = () => {
const navigate = useNavigate();
const dispatch = useDispatch();
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={async (e) => {
e.preventDefault();
await dispatch(addEducation(formData)).then(() => navigate("/dashboard"));
// i have no idea how this works used to work, i removed the navigate function from the addEducation and it does now
}}
>
<div className="form-group">
<input
type="text"
placeholder="* School or Bootcamp"
name="school"
value={school}
onChange={onChange}
required
/>
</div>
<div className="form-group">
<input
type="text"
placeholder="* Degree or Certificate"
name="degree"
value={degree}
onChange={onChange}
required
/>
</div>
<div className="form-group">
<input
type="text"
placeholder="Field of Study"
name="fieldofstudy"
value={fieldofstudy}
onChange={onChange}
/>
</div>
<div className="form-group">
<h4>From Date</h4>
<input type="date" name="from" value={from} onChange={onChange} />
</div>
<div className="form-group">
<p>
<input
type="checkbox"
name="current"
checked={current}
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>
);
};
export default AddEducation;
@@ -1,115 +0,0 @@
import React, { useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { useDispatch } from 'react-redux';
import { addExperience } from '../../actions/profile';
const AddExperience = () => {
const dispatch = useDispatch();
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={async(e) => {
e.preventDefault();
await dispatch(addExperience(formData)).then(() => navigate('/dashboard'));
}}
>
<div className="form-group">
<input
type="text"
placeholder="* Job Title"
name="title"
value={title}
onChange={onChange}
required
/>
</div>
<div className="form-group">
<input
type="text"
placeholder="* Company"
name="company"
value={company}
onChange={onChange}
required
/>
</div>
<div className="form-group">
<input
type="text"
placeholder="Location"
name="location"
value={location}
onChange={onChange}
/>
</div>
<div className="form-group">
<h4>From Date</h4>
<input type="date" name="from" value={from} onChange={onChange} />
</div>
<div className="form-group">
<p>
<input
type="checkbox"
name="current"
checked={current}
value={current}
onChange={() => {
setFormData({ ...formData, current: !current });
}}
/>{' '}
Current Job
</p>
</div>
<div className="form-group">
<h4>To Date</h4>
<input
type="date"
name="to"
value={to}
onChange={onChange}
disabled={current}
/>
</div>
<div className="form-group">
<textarea
name="description"
cols="30"
rows="5"
placeholder="Job Description"
value={description}
onChange={onChange}
/>
</div>
<input type="submit" className="btn btn-primary my-1" />
<Link className="btn btn-light my-1" to="/dashboard">
Go Back
</Link>
</form>
</section>
);
};
export default AddExperience;
@@ -1,266 +0,0 @@
import React, { Fragment, useState, useEffect } from "react";
import { Link, useMatch, useNavigate } from "react-router-dom";
import { createProfile, getCurrentProfile } from "../../actions/profile";
import { useDispatch, useSelector } from "react-redux";
const initialState = {
company: "",
website: "",
location: "",
status: "",
skills: "",
githubusername: "",
bio: "",
twitter: "",
facebook: "",
linkedin: "",
youtube: "",
instagram: "",
};
const ProfileForm = () => {
const dispatch = useDispatch();
const { profile, loading } = useSelector((state) => state.profile);
const [formData, setFormData] = useState(initialState);
const creatingProfile = useMatch("/create-profile");
const [displaySocialInputs, toggleSocialInputs] = useState(false);
const navigate = useNavigate();
useEffect(() => {
// if there is no profile, attempt to fetch one
async function fetchData(){
await dispatch(getCurrentProfile())
}
if (!profile) fetchData();
// if we finished loading and we do have a profile
// then build our profileData
if (!loading && profile) {
const profileData = { ...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, dispatch, 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 = async (e) => {
const editing = profile ? true : false;
e.preventDefault();
await dispatch(createProfile(formData, editing)).then(() => {
console.log(editing)
if (!editing) navigate("/dashboard");
});
};
return (
<section className="container">
<h1 className="large text-primary">
{creatingProfile ? "Create Your Profile" : "Edit Your Profile"}
</h1>
<p className="lead">
<i className="fas fa-user" />
{creatingProfile
? ` Let's get some information to make your`
: " Add some changes to your profile"}
</p>
<small>* = required field</small>
<form className="form" onSubmit={onSubmit}>
<div className="form-group">
<select name="status" value={status} onChange={onChange}>
<option>* Select Professional Status</option>
<option value="Developer">Developer</option>
<option value="Junior Developer">Junior Developer</option>
<option value="Senior Developer">Senior Developer</option>
<option value="Manager">Manager</option>
<option value="Student or Learning">Student or Learning</option>
<option value="Instructor">Instructor or Teacher</option>
<option value="Intern">Intern</option>
<option value="Other">Other</option>
</select>
<small className="form-text">
Give us an idea of where you are at in your career
</small>
</div>
<div className="form-group">
<input
type="text"
placeholder="Company"
name="company"
value={company}
onChange={onChange}
/>
<small className="form-text">
Could be your own company or one you work for
</small>
</div>
<div className="form-group">
<input
type="text"
placeholder="Website"
name="website"
value={website}
onChange={onChange}
/>
<small className="form-text">
Could be your own or a company website
</small>
</div>
<div className="form-group">
<input
type="text"
placeholder="Location"
name="location"
value={location}
onChange={onChange}
/>
<small className="form-text">
City & state suggested (eg. Boston, MA)
</small>
</div>
<div className="form-group">
<input
type="text"
placeholder="* Skills"
name="skills"
value={skills}
onChange={onChange}
/>
<small className="form-text">
Please use comma separated values (eg. HTML,CSS,JavaScript,PHP)
</small>
</div>
<div className="form-group">
<input
type="text"
placeholder="Github Username"
name="githubusername"
value={githubusername}
onChange={onChange}
/>
<small className="form-text">
If you want your latest repos and a Github link, include your
username
</small>
</div>
<div className="form-group">
<textarea
placeholder="A short bio of yourself"
name="bio"
value={bio}
onChange={onChange}
/>
<small className="form-text">Tell us a little about yourself</small>
</div>
<div className="my-2">
<button
onClick={() => toggleSocialInputs(!displaySocialInputs)}
type="button"
className="btn btn-light"
>
Add Social Network Links
</button>
<span>Optional</span>
</div>
{displaySocialInputs && (
<Fragment>
<div className="form-group social-input">
<i className="fab fa-twitter fa-2x" />
<input
type="text"
placeholder="Twitter URL"
name="twitter"
value={twitter}
onChange={onChange}
/>
</div>
<div className="form-group social-input">
<i className="fab fa-facebook fa-2x" />
<input
type="text"
placeholder="Facebook URL"
name="facebook"
value={facebook}
onChange={onChange}
/>
</div>
<div className="form-group social-input">
<i className="fab fa-youtube fa-2x" />
<input
type="text"
placeholder="YouTube URL"
name="youtube"
value={youtube}
onChange={onChange}
/>
</div>
<div className="form-group social-input">
<i className="fab fa-linkedin fa-2x" />
<input
type="text"
placeholder="Linkedin URL"
name="linkedin"
value={linkedin}
onChange={onChange}
/>
</div>
<div className="form-group social-input">
<i className="fab fa-instagram fa-2x" />
<input
type="text"
placeholder="Instagram URL"
name="instagram"
value={instagram}
onChange={onChange}
/>
</div>
</Fragment>
)}
<input type="submit" className="btn btn-primary my-1" />
<Link className="btn btn-light my-1" to="/dashboard">
Go Back
</Link>
</form>
</section>
);
};
export default ProfileForm;
@@ -19,10 +19,10 @@ const initialState = {
company: "", company: "",
website: "", website: "",
location: "", location: "",
status: "",
skills: "", skills: "",
githubusername: "", githubusername: "",
bio: "", bio: "",
status: "",
twitter: "", twitter: "",
facebook: "", facebook: "",
linkedin: "", linkedin: "",
@@ -55,7 +55,7 @@ const ProfileForm = () => {
// then build our profileData // then build our profileData
if (!loading && profile) { if (!loading && profile) {
const profileData: any = { ...initialState }; const profileData: any = { ...initialState };
// cant figure out how to type key to be keyof profile so they are any now // can't figure out how to type key to be keyof profile so they are any now
for (const key in profile) { for (const key in profile) {
if (key in profileData) profileData[key] = profile[key]; if (key in profileData) profileData[key] = profile[key];
} }
@@ -82,16 +82,19 @@ const ProfileForm = () => {
youtube, youtube,
linkedin, linkedin,
instagram, instagram,
status,
} = formData; } = formData;
const onChange = ( const onChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>, e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
) => setFormData({ ...formData, [e.target.name]: e.target.value }); ) => {
return setFormData({ ...formData, [e.target.name]: e.target.value });
};
const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => { const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
const editing = profile ? true : false; const editing = profile ? true : false;
e.preventDefault(); e.preventDefault();
const form = new FormData(e.currentTarget);
const status = form.get("status") as string;
if (facebook?.length > 100) { if (facebook?.length > 100) {
dispatch(createAlert("Facebook link is longer 100 characters", "danger")); dispatch(createAlert("Facebook link is longer 100 characters", "danger"));
} else if (linkedin?.length > 100) { } else if (linkedin?.length > 100) {
@@ -108,9 +111,9 @@ const ProfileForm = () => {
dispatch(createAlert("Skills is longer 100 characters", "danger")); dispatch(createAlert("Skills is longer 100 characters", "danger"));
} else if (skills.length === 0) { } else if (skills.length === 0) {
dispatch(createAlert("Skills is required", "danger")); dispatch(createAlert("Skills is required", "danger"));
} else if (skills.length > 100 || skills.length === 0) {
dispatch(createAlert("Status is required", "danger"));
} else if (status?.length === 0) { } else if (status?.length === 0) {
dispatch(createAlert("Status is required", "danger"));
} else if (location?.length > 100) {
dispatch(createAlert("Location is longer 100 characters", "danger")); dispatch(createAlert("Location is longer 100 characters", "danger"));
} else if (githubusername?.length > 50) { } else if (githubusername?.length > 50) {
dispatch( dispatch(
@@ -212,12 +215,12 @@ const ProfileForm = () => {
</Grid> </Grid>
<Grid size={{ xs: 6 }}> <Grid size={{ xs: 6 }}>
<Typography> <Typography>
Please use comma separeted values (eg. HTML, CSS, JavaScript, PHP) Please use comma separated values (eg. HTML, CSS, JavaScript, PHP)
</Typography> </Typography>
</Grid> </Grid>
<Grid size={{ xs: 6 }}> <Grid size={{ xs: 6 }}>
<TextField <TextField
name="githubUser" name="githubusername"
fullWidth fullWidth
label="Github Username" label="Github Username"
onChange={onChange} onChange={onChange}
@@ -237,9 +240,8 @@ const ProfileForm = () => {
labelId="status" labelId="status"
name="status" name="status"
required required
placeholder="Select Profesional status" placeholder="Select Professional status"
defaultValue={""} defaultValue={""}
value={status}
> >
<MenuItem value="">None</MenuItem> <MenuItem value="">None</MenuItem>
<MenuItem value="Developer">Developer</MenuItem> <MenuItem value="Developer">Developer</MenuItem>
@@ -256,7 +258,7 @@ const ProfileForm = () => {
</Grid> </Grid>
<Grid size={{ xs: 6 }}> <Grid size={{ xs: 6 }}>
<Typography paddingTop="2.3rem"> <Typography paddingTop="2.3rem">
Select Profesional Status Select Professional Status
</Typography> </Typography>
</Grid> </Grid>
<Grid size={{ xs: 12 }}> <Grid size={{ xs: 12 }}>
-86
View File
@@ -1,86 +0,0 @@
import React, { Fragment, useEffect } from "react";
import { Link, useParams } from "react-router-dom";
import { useDispatch, useSelector } 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 = () => {
const profile = useSelector((state) => state.profile.profile);
const auth = useSelector((state) => state.auth);
const dispatch = useDispatch();
const { id } = useParams();
useEffect(() => {
async function fetchData(){
await dispatch(getProfileById(id));
}
fetchData();
}, [dispatch,id]);
return (
<section className="container">
{profile === null ? (
<Spinner />
) : (
<Fragment>
<Link to="/profiles" className="btn btn-light">
Back To Profiles
</Link>
{auth.isAuthenticated &&
auth.loading === false &&
auth.user._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>
);
};
export default Profile;
@@ -1,30 +0,0 @@
import React, { Fragment } from 'react';
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>
);
export default ProfileAbout;
@@ -1,26 +0,0 @@
import React from 'react';
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>
);
export default ProfileEducation;
@@ -1,25 +0,0 @@
import React from 'react';
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>
);
export default ProfileExperience;
@@ -1,45 +0,0 @@
import React, { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { getGithubRepos } from "../../actions/profile";
const ProfileGithub = ({ username }) => {
const repos = useSelector((state) => state.profile.repos);
const dispatch = useDispatch();
useEffect(() => {
async function fetchData() {
await dispatch(getGithubRepos(username));
}
fetchData();
}, [dispatch, username]);
return (
<div className="profile-github">
<h2 className="text-primary my-1">Github Repos</h2>
{repos.map((repo) => (
<div key={repo.id} className="repo bg-white p-1 my-1">
<div>
<h4>
<a href={repo.html_url} target="_blank" rel="noopener noreferrer">
{repo.name}
</a>
</h4>
<p>{repo.description}</p>
</div>
<div>
<ul>
<li className="badge badge-primary">
Stars: {repo.stargazers_count}
</li>
<li className="badge badge-dark">
Watchers: {repo.watchers_count}
</li>
<li className="badge badge-light">Forks: {repo.forks_count}</li>
</ul>
</div>
</div>
))}
</div>
);
};
export default ProfileGithub;
@@ -1,46 +0,0 @@
import React from "react";
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>
);
};
export default ProfileTop;
@@ -1,38 +0,0 @@
import React from 'react';
import { Link } from 'react-router-dom';
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>
);
};
export default ProfileItem;
@@ -1,44 +0,0 @@
import React, { Fragment, useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import Spinner from "../layout/Spinner";
import ProfileItem from "./ProfileItem";
import { getProfiles } from "../../actions/profile";
const Profiles = () => {
const dispatch = useDispatch();
useEffect(() => {
async function fetchData() {
await dispatch(getProfiles());
}
fetchData();
}, [dispatch]);
const { profiles, loading } = useSelector((state) => state.profile);
return (
<section className="container">
{loading ? (
<Spinner />
) : (
<Fragment>
<h1 className="large text-primary">Developers</h1>
<p className="lead">
<i className="fab fa-connectdevelop" /> Browse and connect with
developers
</p>
<div className="profiles">
{profiles.length > 0 ? (
profiles.map((profile) => (
<ProfileItem key={profile._id} profile={profile} />
))
) : (
<h4>No profiles found...</h4>
)}
</div>
</Fragment>
)}
</section>
);
};
export default Profiles;
@@ -1,14 +0,0 @@
import React from "react";
import { Navigate } from "react-router-dom";
import { useSelector } from "react-redux";
import Spinner from "../layout/Spinner";
const PrivateRoute = ({ component: Component }) => {
const { isAuthenticated, loading } = useSelector((state) => state.auth);
if (loading) return <Spinner />;
if (isAuthenticated) return <Component />;
return <Navigate to="/login" />;
};
export default PrivateRoute;
-9
View File
@@ -1,9 +0,0 @@
import React from 'react';
import {createRoot} from 'react-dom/client'
import App from './App';
// Level - 0
//Program stars from Here. Imports and renders App.
const root = createRoot(document.getElementById('root'));
root.render(<App/>)
-20
View File
@@ -1,20 +0,0 @@
import { createSlice } from "@reduxjs/toolkit";
const initialState = [];
const alertSlice = createSlice({
name: "alert",
initialState,
reducers: {
setAlert(state, action) {
return [...state, action.payload];
},
removeAlert(state, action) {
return state.filter((alert) => alert.id !== action.payload);
},
},
});
export const { setAlert, removeAlert } = alertSlice.actions;
export default alertSlice.reducer;
-76
View File
@@ -1,76 +0,0 @@
import { createSlice } from "@reduxjs/toolkit";
const initialState = {
token: localStorage.getItem("token"),
isAuthenticated: null,
loading: true,
user: null,
};
const authSlice = createSlice({
name: "auth",
initialState,
reducers: {
userLoaded(state, action) {
return {
...state,
isAuthenticated: true,
loading: false,
user: action.payload,
};
},
registerSuccess(state, action) {
return {
...state,
...action.payload,
isAuthenticated: true,
loading: false,
};
},
loginSucces(state, action) {
return {
...state,
...action.payload,
isAuthenticated: true,
loading: false,
};
},
accountDeleted(state, action) {
return {
...state,
token: null,
isAuthenticated: false,
loading: false,
user: null,
};
},
authError(state, action) {
return {
...state,
token: null,
isAuthenticated: false,
loading: false,
user: null,
};
},
logOut(state, action) {
return {
...state,
token: null,
isAuthenticated: false,
loading: false,
user: null,
};
},
},
});
export const {
logOut,
userLoaded,
loginSucces,
registerSuccess,
accountDeleted,
authError,
} = authSlice.actions;
export default authSlice.reducer;
-92
View File
@@ -1,92 +0,0 @@
import { createSlice } from "@reduxjs/toolkit";
const initialState = {
posts: [],
post: null,
loading: true,
error: {},
};
const postSlice = createSlice({
name: "post",
initialState,
reducers: {
getPostsAction(state, action) {
return {
...state,
posts: action.payload,
loading: false,
};
},
getPostAction(state, action) {
return {
...state,
post: action.payload,
loading: false,
};
},
addPostAction(state, action) {
return {
...state,
posts: [action.payload, ...state.posts],
loading: false,
};
},
deletePostAction(state, action) {
return {
...state,
posts: state.posts.filter((post) => post._id !== action.payload),
loading: false,
};
},
postError(state, action) {
return {
...state,
error: action.payload,
loading: false,
};
},
updateLikes(state, action) {
return {
...state,
posts: state.posts.map((post) =>
post._id === action.payload.id
? { ...post, likes: action.payload.likes }
: post
),
loading: false,
};
},
addCommentAction(state, action) {
return {
...state,
post: { ...state.post, comments: action.payload },
loading: false,
};
},
removeComment(state, action) {
return {
...state,
post: {
...state.post,
comments: state.post.comments.filter(
(comment) => comment._id !== action.payload
),
},
loading: false,
};
},
},
});
export const {
removeComment,
addCommentAction,
updateLikes,
postError,
deletePostAction,
getPostAction,
getPostsAction,
addPostAction,
} = postSlice.actions;
export default postSlice.reducer;
+1 -1
View File
@@ -7,7 +7,7 @@ interface postState {
post: Post | null; post: Post | null;
loading: boolean; loading: boolean;
error: {}; error: {};
//Todo erros //Todo errors
} }
const initialState: postState = { const initialState: postState = {
posts: [], posts: [],
-75
View File
@@ -1,75 +0,0 @@
import { createSlice } from "@reduxjs/toolkit";
const initialState = {
profile: null,
profiles: [],
repos: [],
loading: true,
error: {},
};
const profileSlice = createSlice({
name: "profile",
initialState,
reducers: {
getProfile(state, action) {
return {
...state,
profile: action.payload,
loading: false,
};
},
updateProfile(state, action) {
return {
...state,
profile: action.payload,
loading: false,
};
},
getProfilesType(state, action) {
return {
...state,
profiles: action.payload,
loading: false,
};
},
profileError(state, action) {
return {
...state,
error: action.payload,
loading: false,
profile: null,
};
},
clearProfile(state, action) {
return {
...state,
profile: null,
repos: [],
};
},
getRepos(state, action) {
return {
...state,
repos: action.payload,
loading: false,
};
},
noRepos(state, action) {
return {
...state,
repos: [],
};
},
},
});
export const {
noRepos,
getRepos,
clearProfile,
profileError,
getProfilesType,
updateProfile,
getProfile,
} = profileSlice.actions;
export default profileSlice.reducer;
-32
View File
@@ -1,32 +0,0 @@
import setAuthToken from './utils/setAuthToken';
import { configureStore } from '@reduxjs/toolkit';
import alertReducer from './reducers/alert';
import authReducer from './reducers/auth';
import profileReducer from './reducers/profile'
import postReducer from './reducers/post'
const store = configureStore({
reducer: {
alert: alertReducer,
auth: authReducer,
profile: profileReducer,
post: postReducer
}})
let currentState = store.getState();
store.subscribe(() => {
// keep track of the previous and current state to compare changes
let previousState = currentState;
currentState = store.getState();
// if the token changes set the value in localStorage and axios headers
if (previousState.auth.token !== currentState.auth.token) {
const token = currentState.auth.token;
setAuthToken(token);
}
});
export default store;
-20
View File
@@ -1,20 +0,0 @@
import axios from 'axios';
import store from '../store';
const api = axios.create({
baseURL: '/api',
headers: {
'Content-Type': 'application/json'
}
});
api.interceptors.response.use(
(res) => res,
(err) => {
if (err.response.status === 401) {
store.dispatch({ type: 'auth/logOut' });
}
return Promise.reject(err);
}
);
export default api;
-5
View File
@@ -1,5 +0,0 @@
function formatDate(date) {
return new Intl.DateTimeFormat().format(new Date(date));
}
export default formatDate;
+1 -1
View File
@@ -3,6 +3,6 @@ import type { TypedUseSelectorHook } from "react-redux";
import type { RootState, AppDispatch } from "../store"; import type { RootState, AppDispatch } from "../store";
// Use throughout your app instead of plain `useDispatch` and `useSelector` // Use throughout your app instead of plain `useDispatch` and `useSelector`
// taken from the redux wiki. Same as normal hook but typed
export const useAppDispatch: () => AppDispatch = useDispatch; export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppDispatch2 = () => useDispatch<AppDispatch>;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector; export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
-15
View File
@@ -1,15 +0,0 @@
import api from './api';
// store our JWT in LS and set axios headers if we do have a token
const setAuthToken = (token) => {
if (token) {
api.defaults.headers.common['x-auth-token'] = token;
localStorage.setItem('token', token);
} else {
delete api.defaults.headers.common['x-auth-token'];
localStorage.removeItem('token');
}
};
export default setAuthToken;
@@ -1,9 +1,9 @@
services: services:
app: app:
image: devcon-frontend-dev image: devcon-frontend
build: build:
context: ./client context: ./client
dockerfile: ./dev.Dockerfile dockerfile: ./Dockerfile
ports: ports:
- 5173:5173 - 5173:5173
environment: environment:
@@ -12,10 +12,10 @@ services:
- ./client:/usr/src/app - ./client:/usr/src/app
server: server:
image: devcon-backend-dev image: devcon-backend
build: build:
context: ./server context: ./server
dockerfile: ./dev.Dockerfile dockerfile: ./Dockerfile
ports: ports:
- 5000:5000 - 5000:5000
volumes: volumes:
+8 -6
View File
@@ -1,19 +1,21 @@
services: services:
app: app:
image: devcon-frontend-dev image: devcon-frontend
build: build:
context: ./client context: ./client
dockerfile: ./dev.Dockerfile dockerfile: ./Dockerfile
ports: ports:
- 3000:3000 - 8080:80
environment:
VITE_BACKEND_URL: http://server:5000
volumes: volumes:
- ./client:/usr/src/app - ./client:/usr/src/app
server: server:
image: devcon-backend-dev image: devcon-backend
build: build:
context: ./server context: ./server
dockerfile: ./dev.Dockerfile dockerfile: ./Dockerfile
ports: ports:
- 5000:5000 - 5000:5000
volumes: volumes:
@@ -22,7 +24,7 @@ services:
MONGO_URL: "mongodb://the_username:the_password@mongo:27017/the_database" MONGO_URL: "mongodb://the_username:the_password@mongo:27017/the_database"
mongo: mongo:
image: mongo image: mongo:4.4
ports: ports:
- 3456:27017 - 3456:27017
environment: environment:
-3
View File
@@ -10,6 +10,3 @@ db.createUser({
}); });
db.createCollection("todos"); db.createCollection("todos");
db.todos.insert({ text: "Write code", done: true });
db.todos.insert({ text: "Learn about containers", done: false });
-27
View File
@@ -1,27 +0,0 @@
const express = require('express');
const connectDB= require('./config/db')
const path = require('path')
const app = express();
connectDB();
app.use(express.json({extended:false}));
app.use('/api/users', require('./routers/api/users'))
app.use('/api/auth', require('./routers/api/auth'))
app.use('/api/profile', require('./routers/api/profile'))
app.use('/api/posts', require('./routers/api/posts'))
// Serve static assets in production
if (process.env.NODE_ENV==='production'){
app.use(express.static('client/build'));
app.get('*',(req, res)=>[
res.sendFile(path.resolve(__dirname, 'client', 'build','index.html'))
])
}
const PORT = process.env.PORT || 5000;
app.listen(PORT,()=> console.log(`Server started on port ${PORT}`));
-2
View File
@@ -2,14 +2,12 @@ FROM node:20
WORKDIR /usr/src/app WORKDIR /usr/src/app
COPY --chown=node:node . . COPY --chown=node:node . .
RUN npm ci RUN npm ci
ENV DEBUG=express:* ENV DEBUG=express:*
USER node USER node
CMD npm start CMD npm start
+1 -1
View File
@@ -10,4 +10,4 @@ ENV DEBUG=express:*
USER node USER node
CMD npm start server CMD npm run server
-25
View File
@@ -1,25 +0,0 @@
services:
mongo:
image: mongo
ports:
- 3456:27017
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: example
MONGO_INITDB_DATABASE: the_database
volumes:
- ../mongo/mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js
- mongo_data:/data/db
server: # The name of the service, can be anything
image: devcon-backend-dev # Declares which image to use
build: . # Declares where to build if image is not found
ports: # Declares the ports to publish
- 5000:5000
volumes:
- ./.:/usr/src/app
environment:
MONGO_URL: "mongodb://the_username:the_password@mongo:27017/the_database"
volumes:
mongo_data:
-20
View File
@@ -1,20 +0,0 @@
services:
app: # The name of the service, can be anything
image: devcon-backend # Declares which image to use
build: . # Declares where to build if image is not found
ports: # Declares the ports to publish
- 3000:3000
mongo:
image: mongo
ports:
- 3456:27017
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: example
MONGO_INITDB_DATABASE: the_database
volumes:
- ./mongo/mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js
- mongo_data:/data/db
volumes:
mongo_data:
+21
View File
@@ -26,6 +26,7 @@
"devDependencies": { "devDependencies": {
"@types/bcryptjs": "^2.4.2", "@types/bcryptjs": "^2.4.2",
"@types/config": "^3.3.0", "@types/config": "^3.3.0",
"@types/cors": "^2.8.17",
"@types/express": "^4.17.17", "@types/express": "^4.17.17",
"@types/gravatar": "^1.8.3", "@types/gravatar": "^1.8.3",
"@types/jsonwebtoken": "^9.0.2", "@types/jsonwebtoken": "^9.0.2",
@@ -116,6 +117,16 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/@types/cors": {
"version": "2.8.17",
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz",
"integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/express": { "node_modules/@types/express": {
"version": "4.17.17", "version": "4.17.17",
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.17.tgz", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.17.tgz",
@@ -552,6 +563,7 @@
"version": "2.8.5", "version": "2.8.5",
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
"integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
"license": "MIT",
"dependencies": { "dependencies": {
"object-assign": "^4", "object-assign": "^4",
"vary": "^1" "vary": "^1"
@@ -2209,6 +2221,15 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"@types/cors": {
"version": "2.8.17",
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz",
"integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/express": { "@types/express": {
"version": "4.17.17", "version": "4.17.17",
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.17.tgz", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.17.tgz",
+3 -5
View File
@@ -4,11 +4,8 @@
"description": "", "description": "",
"main": "server.ts", "main": "server.ts",
"scripts": { "scripts": {
"start": "ts-node server.ts", "start": "npx ts-node server.ts",
"server": "ts-node-dev server.ts", "server": "npx ts-node-dev server.ts"
"client": "npm start --prefix client --trace-depracation",
"dev": "concurrently \"npm run server\" \"npm run client\"",
"render": "NPM_CONFIG_PRODUCTION=false npm install --prefix client && npm run build --prefix client"
}, },
"author": "", "author": "",
"license": "ISC", "license": "ISC",
@@ -30,6 +27,7 @@
"devDependencies": { "devDependencies": {
"@types/bcryptjs": "^2.4.2", "@types/bcryptjs": "^2.4.2",
"@types/config": "^3.3.0", "@types/config": "^3.3.0",
"@types/cors": "^2.8.17",
"@types/express": "^4.17.17", "@types/express": "^4.17.17",
"@types/gravatar": "^1.8.3", "@types/gravatar": "^1.8.3",
"@types/jsonwebtoken": "^9.0.2", "@types/jsonwebtoken": "^9.0.2",
-80
View File
@@ -1,80 +0,0 @@
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;
+26 -31
View File
@@ -1,14 +1,14 @@
import express from 'express' import express from "express";
import bcrypt from 'bcryptjs' import bcrypt from "bcryptjs";
import jwt from 'jsonwebtoken'; import jwt from "jsonwebtoken";
import auth from '../../middleware/auth' import auth from "../../middleware/auth";
import config from 'config' import config from "config";
import { check, validationResult } from "express-validator"; import { check, validationResult } from "express-validator";
import User from '../../models/User' import User from "../../models/User";
import type { Request, Response } from 'express'; import type { Request, Response } from "express";
import { isUserId } from '../../utils'; import { isUserId } from "../../utils";
const router = express.Router(); const router = express.Router();
// @route GET api/auth // @route GET api/auth
@@ -16,19 +16,16 @@ const router = express.Router();
// @access Private // @access Private
router.get("/", auth, async (req: any, res) => { router.get("/", auth, async (req: any, res) => {
try { try {
let user: unknown = null let user: unknown = null;
if (isUserId(req)) { if (isUserId(req)) {
user = await User.findById(req.user.id).select("-password"); user = await User.findById(req.user.id).select("-password");
res.json(user); res.json(user);
} } else {
else { throw new Error("missing id in request");
throw new Error('missing id in request')
} }
} catch (err: unknown) { } catch (err: unknown) {
if (typeof err === 'string') if (typeof err === "string") console.error(err);
console.error(err) else if (err instanceof Error) console.error(err.message);
else if (err instanceof Error)
console.error(err.message);
res.status(500).send("Server Error"); res.status(500).send("Server Error");
} }
}); });
@@ -72,25 +69,23 @@ router.post(
id: user.id, id: user.id,
}, },
}; };
const jwtSecret = config.get('jwtSecret')
if (typeof jwtSecret === 'string') jwt.sign( const jwtSecret = process.env.JWT_SECRET
payload, ? process.env.JWT_SECRET
jwtSecret, : config.get("jwtSecret");
{ expiresIn: 360000 },
(err, token) => { if (typeof jwtSecret === "string")
jwt.sign(payload, jwtSecret, { expiresIn: 360000 }, (err, token) => {
if (err) throw err; if (err) throw err;
res.json({ token }); res.json({ token });
} });
); else throw new Error("Error signing the jwt token");
else throw new Error('Error signing the jwt token')
} catch (err: unknown) { } catch (err: unknown) {
if (typeof err === 'string') if (typeof err === "string") console.error(err);
console.error(err) else if (err instanceof Error) console.error(err.message);
else if (err instanceof Error)
console.error(err.message);
res.status(500).send("Server error"); res.status(500).send("Server error");
} }
} },
); );
module.exports = router module.exports = router;
-221
View File
@@ -1,221 +0,0 @@
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;
+1 -2
View File
@@ -47,6 +47,7 @@ router.post(
auth, auth,
check("status", "Status is required").notEmpty(), check("status", "Status is required").notEmpty(),
check("skills", "Skills is required").notEmpty(), check("skills", "Skills is required").notEmpty(),
check("website", "Not a valid website").isURL(),
async (req, res) => { async (req, res) => {
const errors = validationResult(req); const errors = validationResult(req);
if (!errors.isEmpty()) { if (!errors.isEmpty()) {
@@ -213,7 +214,6 @@ router.put(
// @route DELETE api/profile/experience/:exp_id // @route DELETE api/profile/experience/:exp_id
// @desc Delete experience from profile // @desc Delete experience from profile
// @access Private // @access Private
router.delete("/experience/:exp_id", auth, async (req, res) => { router.delete("/experience/:exp_id", auth, async (req, res) => {
try { try {
if (isUserId(req)) { if (isUserId(req)) {
@@ -273,7 +273,6 @@ router.put(
// @route DELETE api/profile/education/:edu_id // @route DELETE api/profile/education/:edu_id
// @desc Delete education from profile // @desc Delete education from profile
// @access Private // @access Private
router.delete("/education/:edu_id", auth, async (req, res) => { router.delete("/education/:edu_id", auth, async (req, res) => {
try { try {
if (isUserId(req)) { if (isUserId(req)) {
+13 -17
View File
@@ -8,7 +8,6 @@ import User from "../../models/User";
import normalizeUrl from "normalize-url"; import normalizeUrl from "normalize-url";
const router = express.Router(); const router = express.Router();
// @route POST api/users // @route POST api/users
@@ -20,7 +19,7 @@ router.post(
check("email", "Please include a valid email").isEmail(), check("email", "Please include a valid email").isEmail(),
check( check(
"password", "password",
"Please enter a password with 6 or more characters" "Please enter a password with 6 or more characters",
).isLength({ min: 6 }), ).isLength({ min: 6 }),
async (req, res) => { async (req, res) => {
const errors = validationResult(req); const errors = validationResult(req);
@@ -45,7 +44,7 @@ router.post(
r: "pg", r: "pg",
d: "mm", d: "mm",
}), }),
{ forceHttps: true } { forceHttps: true },
); );
user = new User({ user = new User({
@@ -67,24 +66,21 @@ router.post(
}, },
}; };
const jwtSecret = config.get('jwtSecret') const jwtSecret = process.env.JWT_SECRET
if (typeof jwtSecret === 'string') jwt.sign( ? process.env.JWT_SECRET
payload, : config.get("jwtSecret");
jwtSecret,
{ expiresIn: "5 days" }, if (typeof jwtSecret === "string")
(err, token) => { jwt.sign(payload, jwtSecret, { expiresIn: "5 days" }, (err, token) => {
if (err) throw err; if (err) throw err;
res.json({ token }); res.json({ token });
} });
);
} catch (err: unknown) { } catch (err: unknown) {
if (typeof err === 'string') if (typeof err === "string") console.error(err);
console.error(err) else if (err instanceof Error) console.error(err.message);
else if (err instanceof Error)
console.error(err.message);
res.status(500).send("Server error"); res.status(500).send("Server error");
} }
} },
); );
module.exports = router module.exports = router;
+6 -3
View File
@@ -1,10 +1,13 @@
import express from "express"; import express from "express";
import connectDB from "./config/db"; import connectDB from "./config/db";
import path from "path"; import path from "path";
import cors from "cors";
const app = express(); const app = express();
// add cors otherwise fronend cannot access backedn
app.use(cors());
connectDB(); connectDB();
app.use(express.json()); app.use(express.json());
@@ -18,7 +21,7 @@ app.use("/api/posts", require("./routers/api/posts"));
if (process.env.NODE_ENV === "production") { if (process.env.NODE_ENV === "production") {
console.log("in production"); console.log("in production");
app.use(express.static("client/build")); app.use(express.static("client/build"));
app.get("*", (req, res) => [ app.get("*", (_, res) => [
res.sendFile(path.resolve(__dirname, "client", "build", "index.html")), res.sendFile(path.resolve(__dirname, "client", "build", "index.html")),
]); ]);
} }
+2 -3
View File
@@ -3,11 +3,10 @@
# systemctl start mongodb.service # systemctl start mongodb.service
sn=devCon sn=devCon
cd ~/devConnectTS/
tmux new-session -s "$sn" -n etc -d "nvim .; exec zsh" tmux new-session -s "$sn" -n etc -d "nvim .; exec zsh"
cd ~/devConnectTS/client cd client
tmux new-window -t "$sn:2" -n "client" "npm run dev" tmux new-window -t "$sn:2" -n "client" "npm run dev"
cd ~/devConnectTS/server cd ../server
tmux new-window -t "$sn:3" -n "server" "npm run server" tmux new-window -t "$sn:3" -n "server" "npm run server"
tmux select-window -t "$sn:1" tmux select-window -t "$sn:1"