├── 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 |
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 |
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 |
61 |
62 | );
63 |
64 | const signInForm = () => (
65 |
66 |
67 |
Sign In
68 |
69 |
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 |
--------------------------------------------------------------------------------