├── README.md ├── jest-teardown.js ├── babel.config.js ├── jest-setup.js ├── client ├── index.html ├── styles │ ├── style.css │ └── style.scss ├── index.js ├── components │ ├── MainContainer.jsx │ ├── App.jsx │ ├── contentContainer.jsx │ ├── Post.jsx │ ├── PostContainer.jsx │ ├── EditModal.jsx │ ├── CreateNewPost.jsx │ └── Form.jsx ├── slices │ ├── contentContainerSlice.js │ ├── appSlice.js │ ├── postContainerSlice.js │ └── createNewPostSlice.js └── store.js ├── server ├── controllers │ ├── sessionController.js │ ├── cookieController.js │ └── userController.js ├── routes │ └── auth.js ├── modelDB.js ├── server.js └── postController.js ├── __tests__ └── testfile.js ├── webpack.config.js ├── package.json └── .gitignore /README.md: -------------------------------------------------------------------------------- 1 | # bird-nerd -------------------------------------------------------------------------------- /jest-teardown.js: -------------------------------------------------------------------------------- 1 | // module.exports = async (globalConfig) => { 2 | // testServer.close(); 3 | // }; 4 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@babel/preset-env', 4 | ['@babel/preset-react', {runtime: 'automatic'}], 5 | ], 6 | }; -------------------------------------------------------------------------------- /jest-setup.js: -------------------------------------------------------------------------------- 1 | // import regeneratorRuntime from 'regenerator-runtime'; 2 | 3 | // module.exports = () => { 4 | // global.testServer = require('./server'); 5 | // }; 6 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Bird Nerd 7 | 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /client/styles/style.css: -------------------------------------------------------------------------------- 1 | .post { 2 | border: 1px solid black; 3 | margin: 10px; 4 | padding: 10px; 5 | } 6 | /* 7 | .form-container { 8 | display: flex; 9 | justify-content: center; 10 | align-items: center; 11 | } 12 | 13 | .sign-up .sign-in { 14 | border: 1px solid black; 15 | border-radius: 10px; 16 | } */ 17 | -------------------------------------------------------------------------------- /client/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import { Provider } from 'react-redux'; 4 | 5 | import App from './components/App.jsx'; 6 | import store from './store.js'; 7 | 8 | const root = createRoot(document.querySelector('#root')); 9 | 10 | root.render( 11 | 12 | 13 | 14 | ); 15 | -------------------------------------------------------------------------------- /client/components/MainContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import CreateNewPost from './CreateNewPost.jsx'; 3 | import PostContainer from './PostContainer.jsx'; 4 | 5 | const MainContainer = () => { 6 | return ( 7 |
8 | 9 | 10 | {/* */} 11 |
12 | ); 13 | }; 14 | 15 | export default MainContainer; 16 | -------------------------------------------------------------------------------- /client/slices/contentContainerSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit'; 2 | 3 | const contentContainerSlice = createSlice({ 4 | name: 'contentContainer', 5 | 6 | initialState: { 7 | activePost: null, 8 | }, 9 | reducers: { 10 | setActivePost: (state, action) => { 11 | state.activePost = action.payload; 12 | }, 13 | }, 14 | }); 15 | 16 | export const { setActivePost } = contentContainerSlice.actions; 17 | 18 | export default contentContainerSlice.reducer; 19 | -------------------------------------------------------------------------------- /client/store.js: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit'; 2 | import postContainerReducer from './slices/postContainerSlice.js'; 3 | import appReducer from './slices/appSlice.js'; 4 | import createNewPostReducer from './slices/createNewPostSlice.js'; 5 | import contentContainerReducer from './slices/contentContainerSlice.js'; 6 | 7 | export default configureStore({ 8 | reducer: { 9 | postContainer: postContainerReducer, 10 | app: appReducer, 11 | createNewPost: createNewPostReducer, 12 | contentContainer: contentContainerReducer, 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /server/controllers/sessionController.js: -------------------------------------------------------------------------------- 1 | const { User } = require('../modelDB.js'); 2 | const bcrypt = require('bcryptjs'); 3 | 4 | const sessionController = {}; 5 | 6 | // REEM NOTE: THIS CONTROLLER CURRENTLY DOES NOT DO ANYTHING - to be updated 7 | 8 | sessionController.isLoggedIn = (req, res, next) => { 9 | // needs to be improved to be more secure, maybe by logging the cookie into the DB and checking for it 10 | const found = req.cookies.sessionCookie 11 | res.locals.user = found; 12 | return next(); 13 | } 14 | 15 | 16 | module.exports = sessionController; 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /server/routes/auth.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const userController = require('../controllers/userController'); 3 | const cookieController = require('../controllers/cookieController'); 4 | const router = express.Router(); 5 | 6 | router.post('/signup', userController.createUser,cookieController.createCookie, (req, res) => { 7 | return res.status(200).json(res.locals.username); 8 | }); 9 | 10 | router.post('/signin', userController.verifyUser, cookieController.createCookie, (req, res) => { 11 | 12 | return res.redirect('/display_all_posts'); 13 | }); 14 | 15 | module.exports = router; 16 | -------------------------------------------------------------------------------- /__tests__/testfile.js: -------------------------------------------------------------------------------- 1 | import appSliceReducer from '../client/slices/appSlice.js' 2 | 3 | describe('appSliceReducer', ()=>{ 4 | let startState; 5 | const fakeAction = {type: 'NOT_ACTION'}; 6 | 7 | beforeEach(()=>{ 8 | startState = { 9 | currentUser: 'myUser', 10 | isLoggedIn: false, 11 | } 12 | }); 13 | 14 | it('should provide a default state for fake action', () => { 15 | const result = appSliceReducer(startState, fakeAction); 16 | expect(result).toEqual({ 17 | currentUser: 'myUser', 18 | isLoggedIn: false, 19 | }) 20 | }) 21 | 22 | }) -------------------------------------------------------------------------------- /client/components/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import CreateNewPost from './CreateNewPost.jsx'; 3 | // TODO: import components that the frontend team creates 4 | import { useSelector } from 'react-redux'; 5 | import MainContainer from './MainContainer.jsx'; 6 | import Form from './Form.jsx'; 7 | import style from '../styles/style.css'; 8 | 9 | const App = () => { 10 | const isLoggedIn = useSelector((state) => state.app.isLoggedIn); 11 | // return either the signin or main feed depending on whethere the user is logged in or not 12 | return isLoggedIn ? :
; 13 | return ; 14 | }; 15 | 16 | export default App; 17 | -------------------------------------------------------------------------------- /client/components/contentContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useSelector } from 'react-redux'; 3 | 4 | const ContentContainer = () => { 5 | const activePostState = useSelector( 6 | (state) => state.contentContainer.activePost 7 | ); 8 | 9 | if (!activePostState) { 10 | return
Click any post to see full content
; 11 | } 12 | 13 | return ( 14 |
15 |

{activePost.title}

16 |

{activePost.postContent}

17 |

{activePost.birdName}

18 |

{activePost.location}

19 |

{activePost.weatherConditions}

20 |

{activePost.date}

21 |

{activePost.time}

22 |
23 | ); 24 | }; 25 | 26 | export default ContentContainer; 27 | -------------------------------------------------------------------------------- /client/slices/appSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit'; 2 | 3 | export const appSlice = createSlice({ 4 | name: 'app', 5 | initialState: { 6 | currentUser: null, 7 | isLoggedIn: false, 8 | }, 9 | reducers: { 10 | logIn: (state = initialState, action) => { 11 | // retrieve current user from payload 12 | const currentUser = action.payload; 13 | // switch boolean to true 14 | const isLoggedIn = true; 15 | return { ...state, currentUser, isLoggedIn }; 16 | }, 17 | logOut: (state = initialState) => { 18 | const currentUser = null; 19 | const isLoggedIn = false; 20 | return { ...state, currentUser, isLoggedIn }; 21 | }, 22 | }, 23 | }); 24 | 25 | export const { logIn, logOut } = appSlice.actions; 26 | 27 | export default appSlice.reducer; -------------------------------------------------------------------------------- /client/slices/postContainerSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit'; 2 | 3 | export const postContainerSlice = createSlice({ 4 | name: 'postContainer', 5 | initialState: { 6 | posts: [ 7 | // --- example --- 8 | // {_id:0,textContent:'hello',user:'marselena'}, 9 | // {_id:1,textContent:'bye',user:'lillian'} 10 | ], 11 | selected: null, 12 | }, 13 | reducers: { 14 | // Refresh the list of all posts 15 | 16 | refresh: (state, action) => { 17 | const posts = action.payload; 18 | return { ...state, posts }; 19 | }, 20 | // Select a single post from the container 21 | select: (state, action) => { 22 | // get id from payload and store it in state as selected 23 | const selected = action.payload; 24 | return { ...state, selected }; 25 | }, 26 | }, 27 | }); 28 | 29 | export const { refresh, select } = postContainerSlice.actions; 30 | 31 | export default postContainerSlice.reducer; 32 | -------------------------------------------------------------------------------- /server/controllers/cookieController.js: -------------------------------------------------------------------------------- 1 | const { User } = require('../modelDB.js'); 2 | const bcrypt = require('bcryptjs'); 3 | 4 | const cookieController = {}; 5 | 6 | // checks if user is logged in uses their creds 7 | 8 | cookieController.createCookie = (req, res, next) => { 9 | console.log(res.locals.username); 10 | console.log('Origin', req.headers.origin); 11 | res.header('Access-Control-Allow-Credentials', true); 12 | res.header("Access-Control-Allow-Headers", 'date, etag, access-control-allow-origin, access-control-allow-credentials'); 13 | 14 | res.cookie('sessionCookie', `${res.locals.username}`, { 15 | // httpOnly: true, 16 | path:"/", 17 | // domain: "localhost3000", 18 | // secure: false, 19 | SameSite: "None", 20 | maxAge: 3600000, 21 | }); 22 | // res.set('Access-Control-Allow-Origin', req.headers.origin) 23 | // res.set('Access-Control-Allow-Credentials', 'true') 24 | // res.set( 25 | // 'Access-Control-Expose-Headers', 26 | // 'date, etag, access-control-allow-origin, access-control-allow-credentials' 27 | // ); 28 | 29 | return next() 30 | }; 31 | 32 | 33 | 34 | module.exports = cookieController; 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | 4 | module.exports = { 5 | mode: process.env.NODE_ENV, 6 | entry: path.resolve(__dirname, './client/index.js'), 7 | output: { 8 | path: path.resolve(__dirname, './build'), 9 | filename: 'bundle.js', 10 | }, 11 | module: { 12 | rules: [ 13 | { 14 | test: /\.jsx?/, 15 | exclude: /node_modules/, 16 | loader: 'babel-loader', 17 | options: { 18 | presets: ['@babel/env', '@babel/react'], 19 | }, 20 | }, 21 | { 22 | test: /\.s?css/, 23 | use: ['style-loader', 'css-loader', 'sass-loader'], 24 | }, 25 | { 26 | test: /\.(png|jpe?g|gif)$/i, 27 | use: [ 28 | { 29 | loader: 'file-loader', 30 | }, 31 | ], 32 | }, 33 | ], 34 | }, 35 | plugins: [ 36 | new HtmlWebpackPlugin({ 37 | template: path.resolve(__dirname, './client/index.html'), 38 | }), 39 | ], 40 | devServer: { 41 | static: { 42 | publicPath: '/build', 43 | directory: path.resolve(__dirname, './build'), 44 | }, 45 | }, 46 | resolve: { 47 | extensions: ['.js', '.jsx'] 48 | } 49 | // devServer: { 50 | // host: 'localhost', 51 | // port: 8080, 52 | // hot: true, 53 | // static: { 54 | // directory: path.resolve(__dirname, 'build') 55 | // }, 56 | // proxy: [{ 57 | // context: ['/api/**'], 58 | // target: 'http://localhost:3000', 59 | // }, 60 | // ], 61 | // }, 62 | }; 63 | -------------------------------------------------------------------------------- /client/slices/createNewPostSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit'; 2 | 3 | const createNewPostSlice = createSlice({ 4 | name: 'createNewPost', 5 | 6 | initialState: { 7 | title: '', 8 | postContent: '', 9 | birdName: '', 10 | location: '', 11 | weatherConditions: '', 12 | date: '', 13 | time: '', 14 | }, 15 | 16 | reducers: { 17 | updateTitle: (state, action) => { 18 | state.title = action.payload; 19 | }, 20 | updateBody: (state, action) => { 21 | state.postContent = action.payload; 22 | }, 23 | updateNameOfBird: (state, action) => { 24 | state.birdName = action.payload; 25 | }, 26 | updateLocation: (state, action) => { 27 | state.location = action.payload; 28 | }, 29 | updateWeather: (state, action) => { 30 | state.weatherConditions = action.payload; 31 | }, 32 | updateDate: (state, action) => { 33 | state.date = action.payload; 34 | }, 35 | updateTime: (state, action) => { 36 | state.time = action.payload; 37 | }, 38 | reset: (state, action) => { 39 | state.title = ''; 40 | state.postContent = ''; 41 | state.birdName = ''; 42 | state.location = ''; 43 | state.weatherConditions = ''; 44 | state.date = ''; 45 | state.time = ''; 46 | }, 47 | }, 48 | }); 49 | 50 | export const { 51 | updateBody, 52 | updateNameOfBird, 53 | updateLocation, 54 | updateWeather, 55 | updateDate, 56 | updateTime, 57 | updateTitle, 58 | reset, 59 | } = createNewPostSlice.actions; 60 | 61 | export default createNewPostSlice.reducer; 62 | -------------------------------------------------------------------------------- /server/controllers/userController.js: -------------------------------------------------------------------------------- 1 | const { User } = require('../modelDB.js'); 2 | const bcrypt = require('bcryptjs'); 3 | 4 | const userController = {}; 5 | 6 | userController.createUser = (req, res, next) => { 7 | const { username, password } = req.body; 8 | if (!username || !password) { 9 | return next({ 10 | log: 'Missing username or password in userController', 11 | status: 400, 12 | message: { err: 'An error occurred' }, 13 | }); 14 | } 15 | User.create({ username, password }) 16 | .then((data) => { 17 | res.locals.username = data.username; 18 | next(); 19 | }) 20 | .catch((err) => { 21 | return next({ 22 | log: 'Error occurred in create user', 23 | status: 500, 24 | message: { err: 'An error occurred' }, 25 | }); 26 | }); 27 | }; 28 | 29 | userController.verifyUser = (req, res, next) => { 30 | console.log('verifying user'); 31 | const { username, password } = req.body; 32 | if (!username || !password) { 33 | return next({ 34 | log: 'Missing username or password in userController', 35 | status: 400, 36 | message: { err: 'An error occurred' }, 37 | }); 38 | } 39 | User.findOne({ username }) 40 | .then((data) => { 41 | bcrypt.compare(password, data.password).then((result) => { 42 | if (!result) { 43 | // TODO change state to signup 44 | res.redirect('/signup'); 45 | } else { 46 | res.locals.username = data.username; 47 | return next(); 48 | } 49 | }); 50 | }) 51 | .catch((err) => { 52 | return next({ 53 | log: 'Error occurred in verifyUser', 54 | status: 500, 55 | message: { err: 'An error occurred' }, 56 | }); 57 | }); 58 | }; 59 | 60 | 61 | module.exports = userController; 62 | -------------------------------------------------------------------------------- /client/components/Post.jsx: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import { useDispatch } from 'react-redux'; 3 | import { setActivePost } from '../slices/contentContainerSlice'; 4 | import Modal from './EditModal.jsx'; 5 | 6 | const Post = ({ post }) => { 7 | const [displayModal, setDisplayModal] = useState(false) 8 | 9 | const handleCancel = () => { 10 | setDisplayModal(false); 11 | } 12 | 13 | const dispatch = useDispatch(); 14 | // console.log("post", post); 15 | 16 | const handleClick = () => { 17 | dispatch(setActivePost(post)); 18 | }; 19 | 20 | console.log(typeof post.createdAt) 21 | let datePortion = (post.createdAt ? post.createdAt.substring(0,10) : "old datestamp will be updated") 22 | datePortion = new Date(datePortion) 23 | datePortion = datePortion.toDateString(); 24 | 25 | 26 | let timePortion = (post.createdAt ? post.createdAt.substring(11,16) : "old datestamp will be updated") 27 | console.log('time', timePortion) 28 | 29 | 30 | 31 | 32 | return ( 33 |
34 | {datePortion} at {timePortion} 35 | {post.username} 36 |

Bird Name: {post.birdName}

37 |

Weather: {post.weatherConditions}

38 |

Location: {post.location}

39 |

Time of bird sighting: {post.date} at {post.time}

40 |

Details:

41 |

{post.postContent}

42 | 43 | {displayModal && } 49 |
50 | ); 51 | }; 52 | 53 | export default Post; 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bird-nerd", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "NODE_ENV=production nodemon server/server.js", 8 | "build": "webpack", 9 | "dev": "NODE_ENV=development nodemon server/server.js & NODE_ENV=development webpack serve --open", 10 | "predeploy": "npm run build", 11 | "deploy": "NODE_ENV=production gh-pages -d build", 12 | "test": "jest" 13 | }, 14 | "author": "", 15 | "license": "ISC", 16 | "dependencies": { 17 | "@reduxjs/toolkit": "^2.2.1", 18 | "bcrypt": "^5.1.1", 19 | "bcryptjs": "^2.4.3", 20 | "chai": "^5.1.0", 21 | "cookie-parser": "^1.4.6", 22 | "cookie-session": "^2.1.0", 23 | "cors": "^2.8.5", 24 | "dotenv": "^16.4.5", 25 | "express": "^4.18.3", 26 | "express-session": "^1.18.0", 27 | "mongodb": "^6.4.0", 28 | "mongoose": "^8.2.1", 29 | "nodemon": "^3.1.0", 30 | "react": "^18.2.0", 31 | "react-dom": "^18.2.0", 32 | "react-redux": "^9.1.0", 33 | "webpack": "^5.90.3" 34 | }, 35 | "devDependencies": { 36 | "@babel/core": "^7.24.0", 37 | "@babel/plugin-proposal-object-rest-spread": "^7.20.7", 38 | "@babel/preset-env": "^7.24.0", 39 | "@babel/preset-react": "^7.23.3", 40 | "@testing-library/jest-dom": "^6.4.2", 41 | "@testing-library/react": "^14.2.1", 42 | "@testing-library/user-event": "^14.5.2", 43 | "babel-jest": "^29.7.0", 44 | "babel-loader": "^9.1.3", 45 | "css-loader": "^6.10.0", 46 | "file-loader": "^6.2.0", 47 | "gh-pages": "^6.1.1", 48 | "html-webpack-plugin": "^5.6.0", 49 | "jest": "^29.7.0", 50 | "jest-environment-jsdom": "^29.7.0", 51 | "react-test-renderer": "^18.2.0", 52 | "redux-mock-store": "^1.5.4", 53 | "sass": "^1.71.1", 54 | "sass-loader": "^14.1.1", 55 | "style-loader": "^3.3.4", 56 | "webpack-cli": "^5.1.4", 57 | "webpack-dev-server": "^5.0.2" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /client/components/PostContainer.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import Post from './Post.jsx'; 3 | import { refresh } from '../slices/postContainerSlice.js'; 4 | import { useSelector, useDispatch } from 'react-redux'; 5 | 6 | const PostContainer = () => { 7 | const dispatch = useDispatch(); 8 | const posts = useSelector((state) => state.postContainer.posts); 9 | const [usernameFilter, setUsernameFilter] = useState(''); 10 | 11 | const getPosts = () => { 12 | fetch('http://localhost:3000/display_all_posts', {credentials: 'include'}) 13 | .then((results) => { 14 | return results.json(); 15 | }) 16 | .then((json) => { 17 | console.log(json); 18 | dispatch(refresh(json)); 19 | setUsernameFilter(''); 20 | }); 21 | }; 22 | 23 | const handleFilterChange = (e) => { 24 | setUsernameFilter(e.target.value); 25 | }; 26 | 27 | const filterPosts = () => { 28 | fetch(`http://localhost:3000/postsByUser?username=${usernameFilter}`) 29 | .then((results) => results.json()) 30 | .then((filteredPosts) => { 31 | dispatch(refresh(filteredPosts)); 32 | }) 33 | .catch((error) => { 34 | console.error('Error fetching filtered posts:', error); 35 | }); 36 | }; 37 | 38 | useEffect(() => { 39 | getPosts(); 40 | }, []); 41 | 42 | return ( 43 |
44 |
45 |

My Feed

46 |
47 | 48 | 49 | 50 |
51 |
52 | {posts.map((post) => ( 53 | 54 | ))} 55 |
56 | ); 57 | }; 58 | 59 | export default PostContainer; 60 | -------------------------------------------------------------------------------- /server/modelDB.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const bcrypt = require('bcryptjs'); 3 | // const path = require('path'); 4 | require('dotenv').config(); 5 | // console.log(process.env.PASSWORD); 6 | 7 | // const URI = `mongodb+srv://${process.env.USER_NAME}:${process.env.PASSWORD}@cluster0.jbbfxwt.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0`; 8 | const URI = `mongodb+srv://${process.env.USER_NAME}:${process.env.PASSWORD}@birdnerditeration.bz6dvto.mongodb.net/?retryWrites=true&w=majority&appName=BirdNerdIteration`; 9 | console.log(URI); 10 | mongoose 11 | .connect(URI, { 12 | // options for the connect method to parse the URI 13 | useNewUrlParser: true, 14 | useUnifiedTopology: true, 15 | // sets the name of the DB that our collections are part of 16 | dbName: 'birdnerd', 17 | }) 18 | .then(() => console.log('Connected to Mongo DB.')) 19 | .catch((err) => console.log(err)); 20 | 21 | const Schema = mongoose.Schema; 22 | 23 | // user schema; 24 | const SALT_WORK_FACTOR = 10; 25 | const userSchema = new Schema({ 26 | username: { type: String, required: true, unique: true }, 27 | password: { type: String, required: true }, 28 | }); 29 | 30 | userSchema.pre('save', function (next) { 31 | bcrypt.hash(this.password, SALT_WORK_FACTOR, (err, hash) => { 32 | if (err) { 33 | return next(err); 34 | } 35 | this.password = hash; 36 | return next(); 37 | }); 38 | }); 39 | 40 | // comment schema; 41 | const commentSchema = new Schema({ 42 | username: { type: String, required: true }, 43 | comment: String, 44 | }); 45 | 46 | // user schema; 47 | const postSchema = new Schema({ 48 | username: { type: String, required: true }, 49 | // username_id: { 50 | // type: Schema.Types.ObjectId, 51 | // ref: 'user', 52 | // }, 53 | // password: { type: String, required: true }, 54 | postContent: String, 55 | birdName: String, 56 | dateStamp: Date, 57 | location: String, 58 | weatherConditions: String, 59 | date: String, 60 | time: String, 61 | comments: [{ type: Schema.Types.ObjectId, ref: 'comment' }], 62 | }, 63 | {timestamps: true}); 64 | 65 | const User = mongoose.model('user', userSchema); 66 | const Comment = mongoose.model('comment', commentSchema); 67 | const Post = mongoose.model('post', postSchema); 68 | 69 | // exports all the models 70 | module.exports = { 71 | User, 72 | Comment, 73 | Post, 74 | }; 75 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # .env 11 | .env 12 | 13 | # Diagnostic reports (https://nodejs.org/api/report.html) 14 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 15 | 16 | # Runtime data 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | lib-cov 24 | 25 | # Coverage directory used by tools like istanbul 26 | coverage 27 | *.lcov 28 | 29 | # nyc test coverage 30 | .nyc_output 31 | 32 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 33 | .grunt 34 | 35 | # Bower dependency directory (https://bower.io/) 36 | bower_components 37 | 38 | # node-waf configuration 39 | .lock-wscript 40 | 41 | # Compiled binary addons (https://nodejs.org/api/addons.html) 42 | build/Release 43 | 44 | # Dependency directories 45 | node_modules/ 46 | jspm_packages/ 47 | 48 | # Snowpack dependency directory (https://snowpack.dev/) 49 | web_modules/ 50 | 51 | # TypeScript cache 52 | *.tsbuildinfo 53 | 54 | # Optional npm cache directory 55 | .npm 56 | 57 | # Optional eslint cache 58 | .eslintcache 59 | 60 | # Optional stylelint cache 61 | .stylelintcache 62 | 63 | # Microbundle cache 64 | .rpt2_cache/ 65 | .rts2_cache_cjs/ 66 | .rts2_cache_es/ 67 | .rts2_cache_umd/ 68 | 69 | # Optional REPL history 70 | .node_repl_history 71 | 72 | # Output of 'npm pack' 73 | *.tgz 74 | 75 | # Yarn Integrity file 76 | .yarn-integrity 77 | 78 | # dotenv environment variable files 79 | .env 80 | .env.development.local 81 | .env.test.local 82 | .env.production.local 83 | .env.local 84 | 85 | # parcel-bundler cache (https://parceljs.org/) 86 | .cache 87 | .parcel-cache 88 | 89 | # Next.js build output 90 | .next 91 | out 92 | 93 | # Nuxt.js build / generate output 94 | .nuxt 95 | dist 96 | 97 | # Gatsby files 98 | .cache/ 99 | # Comment in the public line in if your project uses Gatsby and not Next.js 100 | # https://nextjs.org/blog/next-9-1#public-directory-support 101 | # public 102 | 103 | # vuepress build output 104 | .vuepress/dist 105 | 106 | # vuepress v2.x temp and cache directory 107 | .temp 108 | .cache 109 | 110 | # Docusaurus cache and generated files 111 | .docusaurus 112 | 113 | # Serverless directories 114 | .serverless/ 115 | 116 | # FuseBox cache 117 | .fusebox/ 118 | 119 | # DynamoDB Local files 120 | .dynamodb/ 121 | 122 | # TernJS port file 123 | .tern-port 124 | 125 | # Stores VSCode versions used for testing VSCode extensions 126 | .vscode-test 127 | 128 | # yarn v2 129 | .yarn/cache 130 | .yarn/unplugged 131 | .yarn/build-state.yml 132 | .yarn/install-state.gz 133 | .pnp.* 134 | -------------------------------------------------------------------------------- /client/components/EditModal.jsx: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import { useSelector, useDispatch } from 'react-redux'; 3 | import { 4 | updateBody, 5 | updateNameOfBird, 6 | updateLocation, 7 | updateWeather, 8 | updateDate, 9 | updateTime, 10 | updateTitle, 11 | reset, 12 | } from '../slices/createNewPostSlice'; 13 | import { refresh } from '../slices/postContainerSlice.js'; 14 | 15 | const Modal = ( {handleCancel, postContent, username, postId} ) => { 16 | const dispatch = useDispatch(); 17 | const [thisPostContent, setThisPostContent] = useState(postContent) 18 | 19 | const handleSubmit = async (e) => { 20 | const updatedPost = { 21 | _id: postId, 22 | newPostContent: thisPostContent 23 | } 24 | e.preventDefault(); 25 | console.log("updatedpost data", updatedPost) 26 | try { 27 | const response = await fetch('http://localhost:3000/edit_post', { 28 | method: 'PATCH', 29 | mode: 'cors', 30 | headers: { 31 | 'Content-Type': 'application/json', 32 | }, 33 | body: JSON.stringify(updatedPost), 34 | }); 35 | console.log("response from edit", response) 36 | if (!response.ok) { 37 | throw new Error('Failed to edit post'); 38 | } 39 | alert('Edited post successfully'); 40 | dispatch(reset()); 41 | } catch (error) { 42 | console.log('Error editing post: ', error); 43 | } 44 | handleCancel(); 45 | getPosts() 46 | }; 47 | 48 | 49 | const getPosts = () => { 50 | fetch('http://localhost:3000/display_all_posts', {credentials: 'include'}) 51 | .then((results) => { 52 | return results.json(); 53 | }) 54 | .then((json) => { 55 | console.log(json); 56 | dispatch(refresh(json)); 57 | }); 58 | }; 59 | 60 | 61 | return( 62 |
63 | 64 |
65 |

{username}

66 | {/* 67 | 68 | 69 | 70 | 71 | 72 | 73 | */} 74 | 75 | 76 |
77 | 78 | 79 |
80 |
81 | 82 |
83 | ) 84 | } 85 | 86 | export default Modal; -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const cookieParser = require('cookie-parser'); 3 | const cookieSession = require('cookie-session'); 4 | const path = require('path'); 5 | var cors = require('cors'); 6 | const { User, Comment, Post } = require('./modelDB'); 7 | 8 | // const userController = require('./controllers/userController'); 9 | const postController = require('./postController'); 10 | const sessionController = require('./controllers/sessionController'); 11 | 12 | 13 | 14 | const app = express(); 15 | const PORT = 3000; 16 | 17 | 18 | const authRouter = require('./routes/auth'); 19 | 20 | app.set('trust proxy', 1); 21 | app.use(cookieParser()); 22 | 23 | app.use(express.json()); 24 | app.use(express.urlencoded()); 25 | 26 | app.use(cors({ 27 | origin: ['http://localhost:3000', 'http://localhost:8080' ], 28 | allowedHeaders: ['Content-Type', 'Authorization'], 29 | credentials: true, 30 | methods: ['POST', 'PUT', 'GET', 'OPTIONS', 'HEAD', 'DELETE', 'PATCH'], 31 | exposedHeaders: ["set-cookie"]})); 32 | 33 | //{credentials: true, origin: 'http://localhost:8080'} 34 | 35 | 36 | // statically serve everything in the build folder on the route '/build' 37 | app.use('/build', express.static(path.join(__dirname, '../build'))); 38 | // serve index.html on the route '/' 39 | 40 | app.use('/auth', authRouter); 41 | app.get('/', (req, res) => { 42 | return res.status(200).sendFile(path.join(__dirname, '../client/index.html')); 43 | }); 44 | 45 | // post req to create username and password; SIGN UP 46 | // get req to find username and password in db and match it; SIGN IN 47 | // get req to find username and send it to client; 48 | 49 | // display all posts; 50 | app.get('/display_all_posts', postController.displayAllPosts, (req, res) => { 51 | res.status(200).json(res.locals.data); 52 | }); 53 | 54 | // display posts by user 55 | app.get('/postsByUser', postController.displayPostsByUser, (req, res) => { 56 | res.status(200).json(res.locals.posts); 57 | }); 58 | 59 | // post request to /post to create a new post; 60 | app.post('/newpost', postController.createNewPost, (req, res) => { 61 | return res.status(201).json(res.locals); 62 | }); 63 | 64 | // edit the post; 65 | app.patch('/edit_post', postController.editPost, (req, res) => { 66 | return res.status(201).json(res.locals.data); 67 | }); 68 | 69 | // delete post; 70 | app.delete('/delete_post', postController.deletePost, (req, res) => { 71 | return res.status(201).json(res.locals.data); 72 | }); 73 | 74 | // add a comment to an existing post 75 | app.post('/comment', postController.addComment, (req, res) => { 76 | return res.status(201).json(res.locals.comment); 77 | }); 78 | 79 | // global error handler; 80 | app.use((err, req, res, next) => { 81 | const defaultErr = { 82 | log: 'Express error handler caught unknown middleware error', 83 | status: 500, 84 | message: { err: 'An error occurred' }, 85 | }; 86 | const errorObj = Object.assign({}, defaultErr, err); 87 | console.log(errorObj.log); 88 | return res.status(errorObj.status).json(errorObj.message); 89 | }); 90 | 91 | app.listen(PORT, console.log(`Server listening on port ${PORT}`)); 92 | -------------------------------------------------------------------------------- /client/components/CreateNewPost.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useSelector, useDispatch } from 'react-redux'; 3 | import { 4 | updateBody, 5 | updateNameOfBird, 6 | updateLocation, 7 | updateWeather, 8 | updateDate, 9 | updateTime, 10 | updateTitle, 11 | reset, 12 | } from '../slices/createNewPostSlice'; 13 | import { refresh } from '../slices/postContainerSlice'; 14 | 15 | const CreateNewPost = () => { 16 | const dispatch = useDispatch(); 17 | const createNewPostState = useSelector((state) => state.createNewPost); 18 | 19 | //also defined in PostContainer but couldn't figure out how to import it properly because it is dependent on dispatch 20 | const getPosts = () => { 21 | fetch('http://localhost:3000/display_all_posts') 22 | .then((results) => { 23 | return results.json(); 24 | }) 25 | .then((json) => { 26 | console.log(json); 27 | dispatch(refresh(json)); 28 | }); 29 | }; 30 | 31 | const handleClientInput = (actionCreator, value) => { 32 | dispatch(actionCreator(value)); 33 | }; 34 | 35 | const handleSubmit = async (e) => { 36 | e.preventDefault(); 37 | try { 38 | fetch('http://localhost:3000/newpost', { 39 | method: 'POST', 40 | mode: 'cors', 41 | credentials: 'include', 42 | headers: { 43 | 'Content-Type': 'application/json', 44 | }, 45 | body: JSON.stringify(createNewPostState), 46 | }) 47 | .then((result) => result.json()) 48 | .then((res) => { 49 | console.log(res); 50 | getPosts(); 51 | dispatch(reset()); 52 | }) 53 | .catch((err) => console.log(err)); 54 | } catch (error) { 55 | console.log('Error creating post: ', error); 56 | } 57 | }; 58 | 59 | return ( 60 |
61 |

Add a New Post

62 |
63 |
64 | handleClientInput(updateTitle, e.target.value)} 70 | /> 71 |
72 |
73 | handleClientInput(updateNameOfBird, e.target.value)} 79 | /> 80 |
81 |
82 | handleClientInput(updateLocation, e.target.value)} 88 | /> 89 |
90 |
91 | handleClientInput(updateWeather, e.target.value)} 97 | /> 98 |
99 |
100 | handleClientInput(updateDate, e.target.value)} 106 | /> 107 |
108 |
109 | handleClientInput(updateTime, e.target.value)} 115 | /> 116 |
117 |
118 | handleClientInput(updateBody, e.target.value)} 123 | /> 124 |
125 | 126 |
127 |
128 | ); 129 | }; 130 | 131 | export default CreateNewPost; 132 | -------------------------------------------------------------------------------- /client/components/Form.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { useDispatch } from 'react-redux'; 3 | import { logIn } from '../slices/appSlice.js'; 4 | import style from '../styles/style.scss'; 5 | 6 | const Form = () => { 7 | const [isSignUp, setIsSignUp] = useState(false); 8 | const dispatch = useDispatch(); 9 | 10 | const signUpForm = () => ( 11 |
12 |
13 |

Sign Up

14 |
15 |
16 | 22 | 28 | 60 |
61 |
62 | ); 63 | 64 | const signInForm = () => ( 65 |
66 |
67 |

Sign In

68 |
69 |
70 | 76 | 82 | 114 |
115 |
116 | ); 117 | 118 | const renderForm = () => { 119 | if (isSignUp) { 120 | return signUpForm(); 121 | } else { 122 | return signInForm(); 123 | } 124 | }; 125 | 126 | const switchForm = () => { 127 | setIsSignUp(!isSignUp); 128 | }; 129 | 130 | return ( 131 |
132 | {renderForm()} 133 | 138 |
139 | ); 140 | }; 141 | 142 | export default Form; 143 | -------------------------------------------------------------------------------- /client/styles/style.scss: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300..800;1,300..800&display=swap"); 2 | 3 | $DeepGreen: #114232; 4 | $GrassGreen: #2c7407; 5 | $Yellow: #fcdc2a; 6 | $PaleYellow: #f9f8d1; 7 | 8 | $font: "Open Sans", sans-serif; 9 | 10 | body { 11 | background-color: $PaleYellow; 12 | display: flex; 13 | justify-content: center; 14 | } 15 | 16 | button { 17 | color: $PaleYellow; 18 | background-color: $DeepGreen; 19 | font-family: $font; 20 | font-size: 5vw; 21 | font-size: larger; 22 | border: none; 23 | &:hover { 24 | background-color: $GrassGreen; 25 | border-style: none; 26 | color: $PaleYellow; 27 | border: none; 28 | } 29 | text-decoration: none; 30 | padding: 0.3vw 1.5vw; 31 | border-radius: 10px; 32 | margin: 1vw; 33 | } 34 | 35 | .form-container { 36 | font-family: $font; 37 | color: $DeepGreen; 38 | display: flex; 39 | flex-direction: column; 40 | justify-content: center; 41 | align-items: center; 42 | } 43 | 44 | .sign-in-form, 45 | .sign-up-form { 46 | margin: 4vw; 47 | // background-color: aqua; 48 | display: flex; 49 | flex-direction: column; 50 | width: 450px; 51 | height: 200px; 52 | margin-bottom: 0; 53 | 54 | } 55 | 56 | .switch-button { 57 | width: 425px; 58 | margin-top: 0; 59 | } 60 | 61 | .sign-in-username, 62 | .sign-up-username { 63 | border: none; 64 | padding: 2vw; 65 | margin: 1vw; 66 | border-radius: 20px; 67 | } 68 | 69 | .sign-in-password, 70 | .sign-up-password { 71 | border: none; 72 | padding: 2vw; 73 | margin: 1vw; 74 | border-radius: 20px; 75 | } 76 | 77 | .sign-up-wrap, 78 | .sign-in-wrap { 79 | font-family: $font; 80 | display: flex; 81 | justify-content: center; 82 | } 83 | 84 | .sign-up-text, 85 | .sign-in-text { 86 | color: $DeepGreen; 87 | text-align: center; 88 | } 89 | .postFormWrap { 90 | font-family: $font; 91 | background-color: lighten($DeepGreen, 10%); 92 | border-radius: 20px; 93 | border: solid black; 94 | width: 85vw; 95 | } 96 | .postFormWrap p{ 97 | font-family: $font; 98 | margin-left: 35px; 99 | margin-bottom: 0px; 100 | font-size: larger; 101 | font-weight: bold; 102 | color: $PaleYellow; 103 | } 104 | 105 | .createPostForm{ 106 | padding: 5px; 107 | display: flex; 108 | flex-direction: column; 109 | justify-content: center; 110 | align-items: center; 111 | margin: 5vw; 112 | margin-top: 1vw; 113 | .time-box, 114 | .date-box, 115 | .weather-box, 116 | .location-box, 117 | .species-box, 118 | .textarea-box, 119 | .title-box { 120 | font-family: $font; 121 | background-color: $PaleYellow; 122 | padding: 5px; 123 | border: none; 124 | border-radius: 5px; 125 | margin: 5px; 126 | width: 75vw; 127 | } 128 | .textarea-box{ 129 | font-family: $font; 130 | height: 10vh; 131 | } 132 | #createPostButton{ 133 | margin-top: 20px; 134 | margin-bottom: 0px; 135 | padding: 10px; 136 | width: 20vw; 137 | } 138 | } 139 | .postContainer{ 140 | margin-top: 20px; 141 | background-color: transparentize($DeepGreen, 0.3); 142 | border: solid black; 143 | border-radius: 20px; 144 | display: flex; 145 | flex-direction: column; 146 | align-items: center; 147 | .postUtils{ 148 | width: 100%; 149 | display: flex; 150 | align-items: center; 151 | justify-content: space-between; 152 | input { 153 | font-family: $font; 154 | font-size: larger; 155 | padding: 0.3vw 1.5vw; 156 | border-radius: 10px; 157 | margin: 1vw; 158 | } 159 | button { 160 | padding: 5px; 161 | width: 100px; 162 | } 163 | p { 164 | font-family: $font; 165 | margin-left: 35px; 166 | font-size: larger; 167 | font-weight: bold; 168 | color: $PaleYellow; 169 | } 170 | } 171 | .post{ 172 | font-family: $font; 173 | width: 75vw; 174 | border: solid 3px $DeepGreen; 175 | border-radius: 20px; 176 | background-color: $PaleYellow; 177 | a { 178 | float: right; 179 | } 180 | p { 181 | margin: 0; 182 | } 183 | 184 | button { 185 | padding: 5px; 186 | width: 100px; 187 | float: right; 188 | 189 | } 190 | .postdetails { 191 | margin-top: 5px; 192 | background-color: transparentize($DeepGreen, 0.7); 193 | border-radius: 5px; 194 | padding: 5px; 195 | height: 10vw; 196 | } 197 | 198 | } 199 | } 200 | 201 | .modalContainer { 202 | position: fixed; 203 | left: 0; 204 | top: 0; 205 | width: 100%; 206 | height: 100%; 207 | display: flex; 208 | align-items: center; 209 | justify-content: center; 210 | background-color: rgba(0, 0, 0, 0.5); 211 | .editModal { 212 | h2{ 213 | margin-right: auto; 214 | margin-bottom: auto; 215 | } 216 | display: flex; 217 | flex-direction: column; 218 | align-items: center; 219 | justify-content: flex-start; 220 | background-color: white; 221 | height: 20em; 222 | width: 30em; 223 | border-radius: 1rem; 224 | padding: 2rem; 225 | font-family: $font; 226 | #editpostContent{ 227 | margin-top: 1rem; 228 | width: 90%; 229 | height: 70%; 230 | } 231 | } 232 | } 233 | 234 | -------------------------------------------------------------------------------- /server/postController.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { User, Comment, Post } = require('./modelDB'); 3 | 4 | const postController = {}; 5 | 6 | // create a new post and return it back to the client; 7 | postController.createNewPost = (req, res, next) => { 8 | const posterName = req.cookies.sessionCookie 9 | // console.log("poster name", posterName) 10 | const { 11 | postContent, 12 | birdName, 13 | location, 14 | weatherConditions, 15 | date, 16 | time, 17 | } = req.body; 18 | // console.log('body', req.body); 19 | Post.create({ 20 | username: `${posterName}`, 21 | postContent, 22 | birdName, 23 | location, 24 | weatherConditions, 25 | date, 26 | time, 27 | }) 28 | .then((data) => { 29 | const { 30 | username, 31 | postContent, 32 | birdName, 33 | dateStamp, 34 | location, 35 | weatherConditions, 36 | date, 37 | time, 38 | } = data; 39 | res.locals = { 40 | username: username, 41 | postContent: postContent, 42 | birdName: birdName, 43 | dateStamp: dateStamp, 44 | location: location, 45 | weatherConditions: weatherConditions, 46 | date: date, 47 | time: time, 48 | }; 49 | console.log("post that was created", res.locals) 50 | return next(); 51 | }) 52 | .catch((err) => { 53 | console.log(err.message); 54 | const error = { 55 | status: 400, 56 | message: 'Post not created', 57 | log: 'Error creating a post in DB', 58 | }; 59 | return next(error); 60 | }); 61 | }; 62 | 63 | // edit existing post; 64 | postController.editPost = (req, res, next) => { 65 | const { _id, newPostContent } = req.body; 66 | console.log("req from editPost controller", req.body) 67 | 68 | // if request from client missing post text error handling; 69 | if (newPostContent === undefined) { 70 | const error = { 71 | status: 406, 72 | log: 'Missing input from client', 73 | message: 'Missing post context', 74 | }; 75 | return next(error); 76 | } 77 | 78 | Post.findOneAndUpdate( 79 | { _id: _id }, 80 | { postContent: newPostContent }, 81 | { new: true } 82 | ) 83 | .then((data) => { 84 | res.locals.data = data; 85 | return next(); 86 | }) 87 | .catch((err) => { 88 | console.log(err.message); 89 | const error = { 90 | status: 406, 91 | message: 'Post not updates', 92 | log: 'Error updating a post in DB', 93 | }; 94 | return next(error); 95 | }); 96 | }; 97 | 98 | // delete a post; 99 | postController.deletePost = (req, res, next) => { 100 | const { _id } = req.body; 101 | console.log(1); 102 | Post.findOneAndDelete({ _id }) 103 | .then((data) => { 104 | console.log('post deleted: ' + data); 105 | return next(); 106 | }) 107 | .catch((err) => { 108 | console.log(err.message); 109 | console.log(2); 110 | const error = { 111 | status: 406, 112 | log: 'Content not found/ not deleted', 113 | message: 'Post was not deleted', 114 | }; 115 | return next(error); 116 | }); 117 | }; 118 | 119 | // add a comment to a post; 120 | postController.addComment = (req, res, next) => { 121 | const { post_id, username_id, comment } = req.body; 122 | 123 | if ( 124 | post_id === undefined || 125 | username_id === undefined || 126 | comment === undefined 127 | ) { 128 | const error = { 129 | status: 406, 130 | log: 'Missing input from client', 131 | message: 'Missing post context', 132 | }; 133 | return next(error); 134 | } 135 | let comment_id; 136 | Comment.create({ username_id, comment }) 137 | .then((data) => { 138 | comment_id = data._id; 139 | res.locals.comment = data.comment; 140 | console.log(res.locals); 141 | }) 142 | .catch((err) => { 143 | console.log(err); 144 | const error = { 145 | status: 406, 146 | log: 'Missing input from client', 147 | message: 'Missing comment context', 148 | }; 149 | return next(error); 150 | }); 151 | 152 | Post.findOne({ _id: post_id }) 153 | .populate('comments') 154 | .then((data) => { 155 | console.log(data); 156 | return next(); 157 | }) 158 | .catch((err) => { 159 | console.log(err); 160 | const error = { 161 | status: 406, 162 | log: 'Unknown error occured on updating the post comments', 163 | message: 'Unknown error occurred', 164 | }; 165 | return next(error); 166 | }); 167 | }; 168 | 169 | // get all posts and return them back to the client; 170 | postController.displayAllPosts = (req, res, next) => { 171 | Post.find({}, null, { limit: 100 }) 172 | .sort({ createdAt: -1 }) 173 | .then((data) => { 174 | res.locals.data = data; 175 | return next(); 176 | }) 177 | .catch((err) => { 178 | console.log(err.message); 179 | const error = { 180 | status: 400, 181 | message: 'Cannot get posts', 182 | log: 'Error fetching posts from DB', 183 | }; 184 | return next(error); 185 | }); 186 | }; 187 | 188 | // get all posts from a specific user, passing username as the query 189 | postController.displayPostsByUser = (req, res, next) => { 190 | const { username } = req.query; 191 | Post.find({ username: username }) 192 | .sort({ createdAt: -1 }) 193 | .then((posts) => { 194 | res.locals.posts = posts; 195 | return next(); 196 | }) 197 | .catch((err) => { 198 | console.log(err.message); 199 | const error = { 200 | status: 500, 201 | message: 'Error fetching posts by user', 202 | log: 'Error fetching posts from DB by user', 203 | }; 204 | return next(error); 205 | }); 206 | }; 207 | 208 | module.exports = postController; 209 | --------------------------------------------------------------------------------