├── .DS_Store ├── .gitignore ├── assets ├── .DS_Store └── img │ ├── brdl.png │ ├── brdl (1).png │ ├── brdl (10).png │ ├── brdl (2).png │ ├── brdl (3).png │ ├── brdl (4).png │ ├── brdl (5).png │ ├── brdl (7).png │ ├── brdl (8).png │ ├── brdl (9).png │ ├── annoyed bird.jpg │ ├── brdl-logo-2-a.png │ ├── brdl-logo-2-b.png │ ├── brdl-logo-2-c.png │ ├── brdl-logo-6-a.png │ ├── brdl-logos-2.jpeg │ ├── brdl-logos-5.jpeg │ └── brdl-logos-6.jpeg ├── babel.config.js ├── jest.config.js ├── server ├── controllers │ ├── geoController.js │ ├── userController.js │ └── birdController.js ├── tokens │ └── tokens.js ├── models │ └── brdlModels.js └── server.js ├── client ├── redux │ ├── store.js │ ├── reducers │ │ ├── index.js │ │ ├── birdsReducer.js │ │ ├── messagesReducer.js │ │ ├── navigationReducer.js │ │ ├── responsesReducer.js │ │ └── textFieldReducer.js │ ├── constants │ │ └── actionTypes.js │ └── actions │ │ └── actions.js ├── index.js ├── react │ ├── containers │ │ ├── ProfileContainer.jsx │ │ └── CommunityContainer.jsx │ └── components │ │ ├── FriendSitings.jsx │ │ ├── CommunitySitings.jsx │ │ ├── NavBar.jsx │ │ ├── App.jsx │ │ ├── Login.jsx │ │ ├── SignUp.jsx │ │ └── UserStats.jsx └── style.css ├── __tests__ └── client │ ├── containers │ └── ProfileContainer.test.jsx │ ├── reducers │ ├── birdsReducer.test.js │ ├── messagesReducer.test.js │ ├── textFieldReducer.test.js │ ├── navigationReducer.test.js │ └── responsesReducer.test.js │ ├── components │ ├── UserStats.test.jsx │ ├── Login.test.jsx │ └── SignUp.test.jsx │ └── actions.test.js ├── index.html ├── webpack.config.js ├── package.json └── Readme.md /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bird-Watchers-LLC/brdl/HEAD/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | package-lock.json 3 | /server/tokens/tokens.js -------------------------------------------------------------------------------- /assets/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bird-Watchers-LLC/brdl/HEAD/assets/.DS_Store -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { presets: ['@babel/preset-env', '@babel/preset-react'] } -------------------------------------------------------------------------------- /assets/img/brdl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bird-Watchers-LLC/brdl/HEAD/assets/img/brdl.png -------------------------------------------------------------------------------- /assets/img/brdl (1).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bird-Watchers-LLC/brdl/HEAD/assets/img/brdl (1).png -------------------------------------------------------------------------------- /assets/img/brdl (10).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bird-Watchers-LLC/brdl/HEAD/assets/img/brdl (10).png -------------------------------------------------------------------------------- /assets/img/brdl (2).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bird-Watchers-LLC/brdl/HEAD/assets/img/brdl (2).png -------------------------------------------------------------------------------- /assets/img/brdl (3).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bird-Watchers-LLC/brdl/HEAD/assets/img/brdl (3).png -------------------------------------------------------------------------------- /assets/img/brdl (4).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bird-Watchers-LLC/brdl/HEAD/assets/img/brdl (4).png -------------------------------------------------------------------------------- /assets/img/brdl (5).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bird-Watchers-LLC/brdl/HEAD/assets/img/brdl (5).png -------------------------------------------------------------------------------- /assets/img/brdl (7).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bird-Watchers-LLC/brdl/HEAD/assets/img/brdl (7).png -------------------------------------------------------------------------------- /assets/img/brdl (8).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bird-Watchers-LLC/brdl/HEAD/assets/img/brdl (8).png -------------------------------------------------------------------------------- /assets/img/brdl (9).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bird-Watchers-LLC/brdl/HEAD/assets/img/brdl (9).png -------------------------------------------------------------------------------- /assets/img/annoyed bird.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bird-Watchers-LLC/brdl/HEAD/assets/img/annoyed bird.jpg -------------------------------------------------------------------------------- /assets/img/brdl-logo-2-a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bird-Watchers-LLC/brdl/HEAD/assets/img/brdl-logo-2-a.png -------------------------------------------------------------------------------- /assets/img/brdl-logo-2-b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bird-Watchers-LLC/brdl/HEAD/assets/img/brdl-logo-2-b.png -------------------------------------------------------------------------------- /assets/img/brdl-logo-2-c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bird-Watchers-LLC/brdl/HEAD/assets/img/brdl-logo-2-c.png -------------------------------------------------------------------------------- /assets/img/brdl-logo-6-a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bird-Watchers-LLC/brdl/HEAD/assets/img/brdl-logo-6-a.png -------------------------------------------------------------------------------- /assets/img/brdl-logos-2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bird-Watchers-LLC/brdl/HEAD/assets/img/brdl-logos-2.jpeg -------------------------------------------------------------------------------- /assets/img/brdl-logos-5.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bird-Watchers-LLC/brdl/HEAD/assets/img/brdl-logos-5.jpeg -------------------------------------------------------------------------------- /assets/img/brdl-logos-6.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bird-Watchers-LLC/brdl/HEAD/assets/img/brdl-logos-6.jpeg -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transform: { 3 | "^.+\\.(js|jsx)$": "babel-jest", 4 | }, 5 | testEnvironment: 'jsdom', 6 | }; -------------------------------------------------------------------------------- /server/controllers/geoController.js: -------------------------------------------------------------------------------- 1 | const { query } = require('express'); 2 | 3 | const geoController = {} 4 | 5 | module.exports = geoController; -------------------------------------------------------------------------------- /server/tokens/tokens.js: -------------------------------------------------------------------------------- 1 | const tokens = {}; 2 | 3 | tokens.eBirdToken = 'er9pqjjuc7rv'; 4 | tokens.elephantSQL = 'postgres://leopcmfn:IHIXlZQwrSwciHroC0atNJX0UjYAQI7F@kashin.db.elephantsql.com/leopcmfn'; 5 | tokens.testDatabase = ''; 6 | 7 | module.exports = tokens; -------------------------------------------------------------------------------- /client/redux/store.js: -------------------------------------------------------------------------------- 1 | import { createStore } from 'redux'; 2 | // import { composeWithDevTools } from 'redux-devtools-extension'; 3 | import reducers from './reducers/index.js'; 4 | 5 | const store = createStore( 6 | reducers 7 | // composeWithDevTools() 8 | ); 9 | 10 | export default store; 11 | -------------------------------------------------------------------------------- /client/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import styles from './style.css'; 4 | import { Provider } from 'react-redux'; 5 | import store from './redux/store'; 6 | import App from './react/components/App.jsx'; 7 | 8 | render( 9 | 10 | 11 | , 12 | document.getElementById('root') 13 | ); 14 | -------------------------------------------------------------------------------- /client/redux/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | 3 | import navigationReducer from './navigationReducer'; 4 | import textFieldReducer from './textFieldReducer'; 5 | import responsesReducer from './responsesReducer'; 6 | import messagesReducer from './messagesReducer'; 7 | import birdsReducer from './birdsReducer'; 8 | 9 | const reducers = combineReducers({ 10 | navigation: navigationReducer, 11 | textField: textFieldReducer, 12 | responses: responsesReducer, 13 | messages: messagesReducer, 14 | birds: birdsReducer, 15 | }); 16 | 17 | export default reducers; 18 | -------------------------------------------------------------------------------- /client/redux/reducers/birdsReducer.js: -------------------------------------------------------------------------------- 1 | import * as types from '../constants/actionTypes'; 2 | 3 | const initialState = { 4 | seenBirds: [], 5 | localBirds: [] 6 | }; 7 | 8 | const birdsReducer = (state = initialState, action) => { 9 | // const stateCopy = { ...state }; 10 | 11 | switch (action?.type) { 12 | case types.UPDATE_SEEN_BIRDS: 13 | return { 14 | ...state, 15 | seenBirds: action.payload, 16 | }; 17 | 18 | case types.UPDATE_LOCAL_BIRDS: 19 | return { 20 | ...state, 21 | localBirds: action.payload, 22 | }; 23 | 24 | default: 25 | return state; 26 | } 27 | }; 28 | 29 | export default birdsReducer; -------------------------------------------------------------------------------- /__tests__/client/containers/ProfileContainer.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render, screen } from '@testing-library/react' 3 | import '@testing-library/jest-dom'; 4 | import { Provider } from 'react-redux'; 5 | 6 | import store from '../../../client/redux/store'; 7 | import ProfileContainer from '../../../client/react/containers/ProfileContainer'; 8 | 9 | describe('ProfileContainer testing suite.', () => { 10 | // beforeAll(() => render( 11 | // 12 | // 13 | // 14 | // )); 15 | 16 | test('Renders the Profile Container', () => { 17 | // console.log(screen); 18 | expect(true).toBeTruthy(); 19 | }) 20 | }) -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | brdl 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /server/models/brdlModels.js: -------------------------------------------------------------------------------- 1 | const tokens = require('../tokens/tokens'); 2 | 3 | const { Pool } = require('pg'); 4 | 5 | const PG_URI = (process.env.NODE_ENV === 'test') ? tokes.testDatabase : tokens.elephantSQL; 6 | 7 | const pool = new Pool({ 8 | connectionString: PG_URI 9 | }); 10 | 11 | 12 | // We export an object that contains a property called query, 13 | // which is a function that returns the invocation of pool.query() after logging the query 14 | // This will be required in the controllers to be the access point to the database 15 | module.exports = { 16 | query: (text, params, callback) => { 17 | console.log('executed query', text); 18 | return pool.query(text, params, callback); 19 | } 20 | }; -------------------------------------------------------------------------------- /client/redux/reducers/messagesReducer.js: -------------------------------------------------------------------------------- 1 | import * as types from '../constants/actionTypes'; 2 | 3 | const initialState = { 4 | communityMessages: [], 5 | friendMessages: [] 6 | }; 7 | 8 | const messagesReducer = (state = initialState, action) => { 9 | // const stateCopy = { ...state }; 10 | 11 | switch (action?.type) { 12 | case types.UPDATE_FRIEND_MESSAGES: 13 | return { 14 | ...state, 15 | friendMessages: action.payload, 16 | }; 17 | 18 | case types.UPDATE_COMMUNITY_MESSAGES: 19 | return { 20 | ...state, 21 | communityMessages: action.payload, 22 | }; 23 | 24 | default: 25 | return state; 26 | } 27 | }; 28 | 29 | export default messagesReducer; -------------------------------------------------------------------------------- /client/redux/constants/actionTypes.js: -------------------------------------------------------------------------------- 1 | export const CHANGE_PAGE = 'CHANGE_PAGE'; 2 | export const LOGIN = 'LOGIN'; 3 | export const SIGN_UP = 'SIGN_UP'; 4 | export const COMMUNITY = 'COMMUNITY'; 5 | export const PROFILE = 'PROFILE'; 6 | export const USERNAME_CHANGE = 'USERNAME_CHANGE'; 7 | export const PASSWORD_CHANGE = 'PASSWORD_CHANGE'; 8 | export const FULL_NAME_CHANGE = 'FULL_NAME_CHANGE'; 9 | export const RESET_FIELDS = 'RESET_FIELDS'; 10 | export const CREATE_ACCOUNT_SUBMIT = 'CREATE_ACCOUNT_SUBMIT'; 11 | export const LOGIN_SUBMIT = 'LOGIN_SUBMIT'; 12 | export const MESSAGE_SUBMIT = 'MESSAGE_SUBMIT'; 13 | export const UPDATE_FRIEND_MESSAGES = 'UPDATE_FRIEND_MESSAGES'; 14 | export const UPDATE_COMMUNITY_MESSAGES = 'UPDATE_COMMUNITY_MESSAGES'; 15 | export const UPDATE_SEEN_BIRDS = 'UPDATE_SEEN_BIRDS'; 16 | export const UPDATE_LOCAL_BIRDS = 'UPDATE_LOCAL_BIRDS'; 17 | export const UPDATE_LOCATION = 'UPDATE_LOCATION'; 18 | -------------------------------------------------------------------------------- /client/redux/reducers/navigationReducer.js: -------------------------------------------------------------------------------- 1 | import * as types from '../constants/actionTypes'; 2 | 3 | const initialState = { 4 | page: 'signUp', 5 | }; 6 | 7 | const navigationReducer = (state = initialState, action) => { 8 | // const stateCopy = { ...state }; 9 | 10 | switch (action?.type) { 11 | case types.CHANGE_PAGE: 12 | return { 13 | ...state, 14 | page: action.payload, 15 | }; 16 | 17 | case types.SIGN_UP: 18 | return { 19 | ...state, 20 | page: 'signUp', 21 | }; 22 | 23 | case types.LOGIN: 24 | return { 25 | ...state, 26 | page: 'login', 27 | }; 28 | 29 | case types.COMMUNITY: 30 | return { 31 | ...state, 32 | page: 'community', 33 | }; 34 | 35 | case types.PROFILE: 36 | return { 37 | ...state, 38 | page: 'profile', 39 | }; 40 | 41 | default: 42 | return state; 43 | } 44 | }; 45 | 46 | export default navigationReducer; 47 | -------------------------------------------------------------------------------- /client/react/containers/ProfileContainer.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import * as actions from '../../redux/actions/actions.js'; 4 | import UserStats from '../components/UserStats.jsx'; 5 | 6 | const mapStateToProps = state => ({ 7 | username: state.textField.username, 8 | }); 9 | 10 | const mapDispatchTopProps = dispatch => ({ 11 | changePageActionCreator: payload => dispatch(actions.changePageActionCreator(payload)), 12 | // changeToCommunityPageActionCreator: () => dispatch(actions.changeToCommunityPageActionCreator()), // Replaced by the one above it 13 | }); 14 | 15 | class ProfileContainer extends Component { 16 | constructor(props) { 17 | super(props); 18 | } 19 | 20 | render() { 21 | return ( 22 |
23 | {/* */} 24 |

Hello, {this.props.username}

25 | 26 |
27 | ); 28 | } 29 | } 30 | 31 | export default connect(mapStateToProps, mapDispatchTopProps)(ProfileContainer); 32 | -------------------------------------------------------------------------------- /__tests__/client/reducers/birdsReducer.test.js: -------------------------------------------------------------------------------- 1 | import birdsReducer from '../../../client/redux/reducers/birdsReducer'; 2 | import * as types from '../../../client/redux/constants/actionTypes'; 3 | 4 | describe('birdsReducer testing suite.', () => { 5 | let testState; 6 | beforeEach(() => testState = { 7 | seenBirds: [], 8 | localBirds: [] 9 | }) 10 | 11 | test('Should set initial state if no arguments are given.', () => { 12 | const result = birdsReducer(); 13 | expect(result).toEqual(testState); 14 | }) 15 | 16 | test('Should set seenBirds if action type is types.UPDATE_SEEN_BIRDS.', () => { 17 | const action = { type: types.UPDATE_SEEN_BIRDS, payload: ['blue jay', 'robin'] }; 18 | const result = birdsReducer(testState, action); 19 | testState.seenBirds = action.payload; 20 | expect(result).toEqual(testState); 21 | }) 22 | 23 | test('Should set localBirds if action type is types.UPDATE_LOCAL_BIRDS.', () => { 24 | const action = { type: types.UPDATE_LOCAL_BIRDS, payload: ['blue jay', 'robin'] }; 25 | const result = birdsReducer(testState, action); 26 | testState.localBirds = action.payload; 27 | expect(result).toEqual(testState); 28 | }) 29 | }) -------------------------------------------------------------------------------- /client/react/containers/CommunityContainer.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import CommunitSitings from '../components/CommunitySitings.jsx'; 4 | import FriendSitings from '../components/FriendSitings.jsx'; 5 | import * as actions from '../../redux/actions/actions.js'; 6 | 7 | const mapDispatchToProps = dispatch => ({ 8 | changePageActionCreator: payload => dispatch(actions.changePageActionCreator(payload)), 9 | // changeToProfilePageActionCreator: () => dispatch(actions.changeToProfilePageActionCreator()), // replaced by the one above 10 | }); 11 | 12 | class CommunityContainer extends Component { 13 | constructor(props) { 14 | super(props); 15 | } 16 | 17 | render() { 18 | return ( 19 |
20 | {/* */} 21 |

Community Sightings

22 | 23 |

Friend Sightings

24 | 25 |
26 | ); 27 | } 28 | } 29 | 30 | export default connect(null, mapDispatchToProps)(CommunityContainer); 31 | -------------------------------------------------------------------------------- /__tests__/client/reducers/messagesReducer.test.js: -------------------------------------------------------------------------------- 1 | import messagesReducer from '../../../client/redux/reducers/messagesReducer'; 2 | import * as types from '../../../client/redux/constants/actionTypes'; 3 | 4 | describe('messagesReducer testing suite.', () => { 5 | let testState; 6 | beforeEach(() => testState = { 7 | communityMessages: [], 8 | friendMessages: [] 9 | }) 10 | 11 | test('Should set initial state if no arguments are given.', () => { 12 | const result = messagesReducer(); 13 | expect(result).toEqual(testState); 14 | }) 15 | 16 | test('Should set communityMessages if action type is types.UPDATE_COMMUNITY_MESSAGES.', () => { 17 | const action = { type: types.UPDATE_COMMUNITY_MESSAGES, payload: ['hello', 'world'] }; 18 | const result = messagesReducer(testState, action); 19 | testState.communityMessages = action.payload; 20 | expect(result).toEqual(testState); 21 | }) 22 | 23 | test('Should set friendMessages if action type is types.UPDATE_FRIEND_MESSAGES.', () => { 24 | const action = { type: types.UPDATE_FRIEND_MESSAGES, payload: ['hello', 'world'] }; 25 | const result = messagesReducer(testState, action); 26 | testState.friendMessages = action.payload; 27 | expect(result).toEqual(testState); 28 | }) 29 | }) -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | 4 | module.exports = { 5 | mode: 'development', 6 | entry: path.join(__dirname, './client/index.js'), 7 | output: { 8 | path: path.resolve(__dirname, 'build'), 9 | }, 10 | plugins: [ 11 | new HtmlWebpackPlugin({ template: path.join(__dirname, 'index.html'), filename: 'index.html' }), 12 | ], 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.?jsx?/, 17 | exclude: /node_modules/, 18 | use: [ 19 | { 20 | loader: 'babel-loader', 21 | options: { 22 | presets: ['@babel/preset-env', '@babel/preset-react'], 23 | }, 24 | }, 25 | ], 26 | }, 27 | { 28 | test: /\.css$/i, 29 | use: ['style-loader', 'css-loader'], 30 | }, 31 | // { 32 | // test: /\.png/, 33 | // type: 'asset/resource' 34 | // } 35 | { 36 | test: /\.(png|svg|jpe?g|gif)$/i, 37 | // include: path.resolve(__dirname, './assets/img'), 38 | // type: 'asset/resource', 39 | 40 | loader: 'file-loader', 41 | 42 | // options: { 43 | // publicPath: path.resolve(__dirname, './build'), 44 | // }, 45 | // }, 46 | }, 47 | ], 48 | }, 49 | devServer: { 50 | static: { 51 | directory: path.resolve(__dirname, './build'), 52 | publicPath: path.resolve(__dirname, './build'), 53 | }, 54 | }, 55 | }; 56 | -------------------------------------------------------------------------------- /client/redux/reducers/responsesReducer.js: -------------------------------------------------------------------------------- 1 | import * as types from '../constants/actionTypes'; 2 | 3 | const testMessages = [ 4 | { username: 'Kirk', location: { area: 'Lumberton' }, sciName: 'Robin', timeStamp: '1pm' }, 5 | { username: 'Justin', location: { area: 'Beaumont' }, sciName: 'Blue Jay', timeStamp: '2pm' }, 6 | { username: 'Calvin', location: { area: 'LA' }, sciName: 'Hawk', timeStamp: '2pm' }, 7 | { username: 'Julia', location: { area: 'LA' }, sciName: 'Eagle', timeStamp: '4pm' }, 8 | ]; 9 | 10 | const testSeenBirds = [ 11 | { sciName: 'Robin', timeStamp: '2pm' }, 12 | { sciName: 'Blue Jay', timeStamp: '2pm' }, 13 | { sciName: 'Eagle', timeStamp: '2pm' }, 14 | { sciName: 'Hawk', timeStamp: '2pm' }, 15 | { sciName: 'Swallow', timeStamp: '2pm' }, 16 | { sciName: 'Crane', timeStamp: '2pm' }, 17 | ]; 18 | 19 | const testLocalBirds = [ 20 | { sciName: 'Robin' }, 21 | { sciName: 'Blue Jay' }, 22 | { sciName: 'Eagle' }, 23 | { sciName: 'Hawk' }, 24 | { sciName: 'Swallow' }, 25 | { sciName: 'Crane' }, 26 | { sciName: 'Pelican' }, 27 | ]; 28 | 29 | const initialState = { 30 | mode: 'prod', 31 | signUpPost: { 32 | valid: true, 33 | }, 34 | loginGet: { 35 | valid: true, 36 | }, 37 | testMessages: testMessages, 38 | testSeenBirds: testSeenBirds, 39 | testLocalBirds: testLocalBirds, 40 | }; 41 | 42 | const responsesReducer = (state = initialState, action) => { 43 | switch (action?.type) { 44 | default: 45 | return state; 46 | } 47 | }; 48 | 49 | export default responsesReducer; 50 | -------------------------------------------------------------------------------- /__tests__/client/components/UserStats.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render, screen } from '@testing-library/react' 3 | import '@testing-library/jest-dom'; 4 | import { Provider } from 'react-redux'; 5 | 6 | import store from '../../../client/redux/store'; 7 | import UserStats from '../../../client/react/components/UserStats'; 8 | import * as types from '../../../client/redux/constants/actionTypes'; 9 | 10 | describe('userStats testing suite.', () => { 11 | const birds = [{ sciName: 'a', }, { sciName: 'b' }, { sciName: 'c' }] 12 | beforeEach(() => render( 13 | 14 | 15 | 16 | )); 17 | 18 | test('Renders the User Stats page', () => { 19 | expect(screen).toBeDefined(); 20 | }) 21 | 22 | test('Should have an h2 tag', () => { 23 | expect(screen.getByTestId('div').childNodes[0].nodeName).toBe('H2'); 24 | }) 25 | 26 | test('Should say how many birds are in area', () => { 27 | expect(screen.getByTestId('div').childNodes[0].innerHTML.includes('0')).toBeTruthy(); 28 | }) 29 | 30 | test('Should update how many birds are in area', () => { 31 | store.dispatch({ type: types.UPDATE_LOCAL_BIRDS, payload: birds }); 32 | expect(screen.getByTestId('div').childNodes[0].innerHTML.includes('3')).toBeTruthy(); 33 | }) 34 | 35 | test('Should update how many birds have been seen', () => { 36 | store.dispatch({ type: types.UPDATE_SEEN_BIRDS, payload: [...birds, { sciName: 'd' }] }); 37 | store.dispatch({ type: types.UPDATE_LOCAL_BIRDS, payload: birds }); 38 | expect(screen.getByTestId('div').childNodes[0].innerHTML.includes('4')).toBeTruthy(); 39 | }) 40 | }) -------------------------------------------------------------------------------- /__tests__/client/reducers/textFieldReducer.test.js: -------------------------------------------------------------------------------- 1 | import textFieldReducer from '../../../client/redux/reducers/textFieldReducer'; 2 | import * as types from '../../../client/redux/constants/actionTypes'; 3 | 4 | describe('textFieldReducer testing suite.', () => { 5 | let testState; 6 | beforeEach(() => testState = { 7 | username: '', 8 | password: '', 9 | fullName: '', 10 | message: '', 11 | response: { valid: false }, 12 | validUser: undefined, 13 | validLogin: undefined, 14 | lat: '', 15 | long: '', 16 | }) 17 | 18 | test('Should set initial state if no arguments are given.', () => { 19 | const result = textFieldReducer(); 20 | expect(result).toEqual(testState); 21 | }) 22 | 23 | test('Should update lat and long if action.type = types.UPDATE_LOCATION', () => { 24 | const action = { type: types.UPDATE_LOCATION, payload: { lat: '25', long: '40' } }; 25 | const result = textFieldReducer(testState, action); 26 | testState.lat = action.payload.lat; 27 | testState.long = action.payload.long; 28 | expect(result).toEqual(testState); 29 | }) 30 | 31 | test('Should change validUser to false if action.type = types.CREATE_ACCOUNT_SUBMIT', () => { 32 | const action = { type: types.CREATE_ACCOUNT_SUBMIT }; 33 | const result = textFieldReducer(testState, action); 34 | testState.validUser = false; 35 | expect(result).toEqual(testState); 36 | }) 37 | 38 | test('Should change validUser to false if action.type = types.LOGIN_SUBMIT', () => { 39 | const action = { type: types.LOGIN_SUBMIT }; 40 | const result = textFieldReducer(testState, action); 41 | testState.validLogin = false; 42 | expect(result).toEqual(testState); 43 | }) 44 | }) -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scratch-project", 3 | "version": "1.0.0", 4 | "description": "This is for the birds", 5 | "main": "server/server.js", 6 | "scripts": { 7 | "start": "nodemon server/server.js", 8 | "test": "jest", 9 | "dev": "SET NODE_ENV=development concurrently \"cross-env webpack-dev-server --open --hot --progress --color \" \"nodemon ./server/server.js\"", 10 | "build": "webpack" 11 | }, 12 | "nodemonConfig": { 13 | "ignore": [ 14 | "build", 15 | "client" 16 | ] 17 | }, 18 | "keywords": [ 19 | "Birds", 20 | "Binoculars", 21 | "Best Scratch Group" 22 | ], 23 | "author": "Me", 24 | "license": "ISC", 25 | "dependencies": { 26 | "axios": "^0.25.0", 27 | "babel-jest": "^27.5.1", 28 | "bcrypt": "^5.0.1", 29 | "browserify": "^17.0.0", 30 | "express": "^4.17.2", 31 | "jest": "^27.5.1", 32 | "node-fetch": "^3.2.0", 33 | "pg": "^8.7.1", 34 | "prop-types": "^15.6.1", 35 | "react": "^17.0.2", 36 | "react-dom": "^17.0.2", 37 | "react-redux": "^7.2.2", 38 | "redux": "^4.0.5" 39 | }, 40 | "devDependencies": { 41 | "@babel/core": "^7.16.12", 42 | "@babel/plugin-transform-runtime": "^7.16.10", 43 | "@babel/preset-env": "^7.16.11", 44 | "@babel/preset-react": "^7.16.7", 45 | "@babel/runtime": "^7.16.7", 46 | "@testing-library/jest-dom": "^5.16.3", 47 | "@testing-library/react": "^12.1.4", 48 | "babel-loader": "^8.2.3", 49 | "concurrently": "^6.0.2", 50 | "cross-env": "^7.0.3", 51 | "css-loader": "^6.5.1", 52 | "file-loader": "^6.2.0", 53 | "html-webpack-plugin": "^5.5.0", 54 | "isomorphic-fetch": "^3.0.0", 55 | "node-sass": "^7.0.1", 56 | "nodemon": "^2.0.7", 57 | "sass": "^1.49.0", 58 | "sass-loader": "^12.4.0", 59 | "style-loader": "^3.3.1", 60 | "webpack": "^5.67.0", 61 | "webpack-cli": "^4.9.2", 62 | "webpack-dev-server": "^4.7.3" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /__tests__/client/reducers/navigationReducer.test.js: -------------------------------------------------------------------------------- 1 | import navigationReducer from '../../../client/redux/reducers/navigationReducer'; 2 | import * as types from '../../../client/redux/constants/actionTypes'; 3 | 4 | describe('navigationReducer testing suite.', () => { 5 | let testState; 6 | beforeEach(() => testState = { 7 | page: 'signUp' 8 | }) 9 | 10 | test('Should set initial state if no arguments are given.', () => { 11 | const result = navigationReducer(); 12 | expect(result).toEqual(testState); 13 | }) 14 | 15 | test('Should set page if action type is types.CHANGE_PAGE.', () => { 16 | const action = { type: types.CHANGE_PAGE, payload: 'login' }; 17 | const result = navigationReducer(testState, action); 18 | testState.page = action.payload; 19 | expect(result).toEqual(testState); 20 | }) 21 | 22 | test('Should set page to signUp if action.type = types.SIGN_UP.', () => { 23 | const action = { type: types.SIGN_UP }; 24 | const result = navigationReducer(testState, action); 25 | testState.page = 'signUp'; 26 | expect(result).toEqual(testState); 27 | }) 28 | 29 | test('Should set page to login if action.type = types.LOGIN.', () => { 30 | const action = { type: types.LOGIN }; 31 | const result = navigationReducer(testState, action); 32 | testState.page = 'login'; 33 | expect(result).toEqual(testState); 34 | }) 35 | 36 | test('Should set page to community if action.type = types.COMMUNITY.', () => { 37 | const action = { type: types.COMMUNITY }; 38 | const result = navigationReducer(testState, action); 39 | testState.page = 'community'; 40 | expect(result).toEqual(testState); 41 | }) 42 | 43 | test('Should set page to profile if action.type = types.PROFILE.', () => { 44 | const action = { type: types.PROFILE }; 45 | const result = navigationReducer(testState, action); 46 | testState.page = 'profile'; 47 | expect(result).toEqual(testState); 48 | }) 49 | }) -------------------------------------------------------------------------------- /__tests__/client/components/Login.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render, screen } from '@testing-library/react' 3 | import '@testing-library/jest-dom'; 4 | import { Provider } from 'react-redux'; 5 | 6 | import store from '../../../client/redux/store'; 7 | import Login from '../../../client/react/components/Login'; 8 | 9 | describe('Login testing suite.', () => { 10 | beforeEach(() => render( 11 | 12 | 13 | 14 | )); 15 | 16 | test('Renders the Profile Container', () => { 17 | expect(screen).toBeTruthy(); 18 | }) 19 | 20 | test('Header exists.', () => { 21 | expect(screen.queryByTestId('header')).toBeDefined(); 22 | }) 23 | 24 | test('Header contains an h1 and p tag', () => { 25 | const children = screen.queryByTestId('header').childNodes; 26 | expect(children[0].nodeName).toBe('H1'); 27 | expect(children[1].nodeName).toBe('P'); 28 | }) 29 | 30 | test('Form exists', () => { 31 | expect(screen.queryByTestId('form')).toBeDefined(); 32 | }) 33 | 34 | test('Form has two labels, a button, and a p tag', () => { 35 | const children = screen.queryByTestId('form').childNodes; 36 | expect(children[0].nodeName).toBe('LABEL'); 37 | expect(children[1].nodeName).toBe('LABEL'); 38 | expect(children[2].nodeName).toBe('BUTTON'); 39 | expect(children[3].nodeName).toBe('P'); 40 | }) 41 | 42 | test('Form button submits the form data', () => { 43 | const children = screen.queryByTestId('form').childNodes; 44 | expect(children[2].type).toBe('submit'); 45 | }) 46 | 47 | test('Form has a username input', () => { 48 | const input = screen.getByText('Username:').nextSibling; 49 | expect(input.type).toBe('text'); 50 | expect(input.placeholder).toBe('enter username'); 51 | }) 52 | 53 | test('Form has a password input', () => { 54 | const input = screen.getByText('Password:').nextSibling; 55 | expect(input.type).toBe('password'); 56 | expect(input.placeholder).toBe('enter password'); 57 | }) 58 | }) -------------------------------------------------------------------------------- /client/redux/actions/actions.js: -------------------------------------------------------------------------------- 1 | import * as types from '../constants/actionTypes.js'; 2 | 3 | export const changePageActionCreator = pl => ({ 4 | type: types.CHANGE_PAGE, 5 | payload: pl, 6 | }); 7 | 8 | export const changeToLoginPageActionCreator = () => ({ 9 | type: types.LOGIN, 10 | }); 11 | 12 | export const changeToSignUpPageActionCreator = () => ({ 13 | type: types.SIGN_UP, 14 | }); 15 | 16 | export const changeToCommunityPageActionCreator = () => ({ 17 | type: types.COMMUNITY, 18 | }); 19 | 20 | export const changeToProfilePageActionCreator = () => ({ 21 | type: types.PROFILE, 22 | }); 23 | 24 | export const usernameChangeActionCreator = e => ({ 25 | type: types.USERNAME_CHANGE, 26 | payload: e, 27 | }); 28 | 29 | export const passwordChangeActionCreator = e => ({ 30 | type: types.PASSWORD_CHANGE, 31 | payload: e, 32 | }); 33 | 34 | export const fullNameChangeActionCreator = e => ({ 35 | type: types.FULL_NAME_CHANGE, 36 | payload: e, 37 | }); 38 | 39 | export const resetFieldsActionCreator = () => ({ 40 | type: types.RESET_FIELDS, 41 | }); 42 | 43 | export const createAccountSubmitActionCreator = (e, mode, serverRes) => ({ 44 | type: types.CREATE_ACCOUNT_SUBMIT, 45 | payload: { e, mode, serverRes }, 46 | }); 47 | 48 | export const loginSubmitActionCreator = (e, mode, serverRes) => ({ 49 | type: types.LOGIN_SUBMIT, 50 | payload: { e, mode, serverRes }, 51 | }); 52 | 53 | export const updateSeenBirdsActionCreator = pl => ({ 54 | type: types.UPDATE_SEEN_BIRDS, 55 | payload: pl, 56 | }); 57 | 58 | export const updateLocalBirdsActionCreator = pl => ({ 59 | type: types.UPDATE_LOCAL_BIRDS, 60 | payload: pl, 61 | }); 62 | 63 | export const updateFriendMessagesActionCreator = pl => ({ 64 | type: types.UPDATE_FRIEND_MESSAGES, 65 | payload: pl, 66 | }); 67 | 68 | export const updateCommunityMessagesActionCreator = pl => ({ 69 | type: types.UPDATE_COMMUNITY_MESSAGES, 70 | payload: pl, 71 | }); 72 | 73 | export const updateLocationActionCreator = pl => ({ 74 | type: types.UPDATE_LOCATION, 75 | payload: pl, 76 | }); 77 | -------------------------------------------------------------------------------- /__tests__/client/components/SignUp.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render, screen } from '@testing-library/react' 3 | import '@testing-library/jest-dom'; 4 | import { Provider } from 'react-redux'; 5 | 6 | import store from '../../../client/redux/store'; 7 | import SignUp from '../../../client/react/components/SignUp'; 8 | 9 | describe('SignUp testing suite.', () => { 10 | beforeEach(() => render( 11 | 12 | 13 | 14 | )); 15 | 16 | test('Renders the Profile Container', () => { 17 | expect(screen).toBeTruthy(); 18 | }) 19 | 20 | test('Header exists.', () => { 21 | expect(screen.queryByTestId('header')).toBeDefined(); 22 | }) 23 | 24 | test('Header contains an h1 and p tag', () => { 25 | const children = screen.queryByTestId('header').childNodes; 26 | expect(children[0].nodeName).toBe('H1'); 27 | expect(children[1].nodeName).toBe('P'); 28 | }) 29 | 30 | test('Form exists', () => { 31 | expect(screen.queryByTestId('form')).toBeDefined(); 32 | }) 33 | 34 | test('Form has two labels, a button, and a p tag', () => { 35 | const children = screen.queryByTestId('form').childNodes; 36 | expect(children[0].nodeName).toBe('LABEL'); 37 | expect(children[1].nodeName).toBe('LABEL'); 38 | expect(children[2].nodeName).toBe('LABEL'); 39 | expect(children[3].nodeName).toBe('BUTTON'); 40 | expect(children[4].nodeName).toBe('P'); 41 | }) 42 | 43 | test('Form button submits the form data', () => { 44 | const children = screen.queryByTestId('form').childNodes; 45 | expect(children[3].type).toBe('submit'); 46 | }) 47 | 48 | test('Form has a username input', () => { 49 | const input = screen.getByText('Create a username:').nextSibling; 50 | expect(input.type).toBe('text'); 51 | expect(input.placeholder).toBe('enter username'); 52 | }) 53 | 54 | test('Form has a password input', () => { 55 | const input = screen.getByText('Create a password:').nextSibling; 56 | expect(input.type).toBe('password'); 57 | expect(input.placeholder).toBe('enter password'); 58 | }) 59 | }) -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | New way of changing pages 2 | 3 | const mapStateToProps = dispatch() { 4 | changePageActionCreator = (payload) => dispatch(changePageActionCreator(payload)) 5 | } // Payload will be a string representing the page name (see the return statement in ./client/react/components/App.jsx to see what page will be loaded by what string) 6 | 7 | If using a button you should use onClick{() => changePageActionCreator('Desired Page')} 8 | 9 | Front to Back/Back to Front nomenclature 10 | 11 | req.query = {} // { username: value, password: value } 12 | username = string (unique to user) 13 | message = string 14 | comBirdName = string 15 | sciBirdName = string 16 | lat: value // to back end 17 | long: value // to back end 18 | location = { area: value } // to front end 19 | timeStamp 20 | 21 | Signing up or Logging In 22 | 23 | /gainAccess* 24 | Get or Post 25 | /gainAccess?username=value&password=value 26 | { username: value, password: value } 27 | response = { valid: boolean } 28 | 29 | Pages 30 | 31 | /community* 32 | GET 33 | /community/everyone?username=value&location=value 34 | { username: value, lat: value, long: value } 35 | response = { messages: [ { username: value, location: { area: value }, comBirdName: value, timeStamp: value}, {...}, ... ] } 36 | 37 | /profile* 38 | GET, POST, or DELETE 39 | GET 40 | /profile?username=value&lat=value&long=value 41 | { username: value, lat: value, long: value } 42 | response = { 43 | birds: [ { comBirdName: value, sciBirdName: value }, {...}, ... ], 44 | seenBirds: [ { comBirdName: value, sciBirdName: value, timeStamp: value }. {...}, ... ] 45 | } 46 | POST 47 | /profile/self?username=value&lat=value&long-value&timeStamp=value&sciBirdName=value // lat/long to two decimal points 48 | { username: value, lat: value, long: value, timeStamp: value, commBirdName, sciBirdName } 49 | response = { valid: boolean } 50 | POST 51 | /profile/community?username=value&location=value&timeStamp=value&message=value... 52 | { username: value, lat: value, long: value, timeStamp: value, commBirdName, sciBirdName, message } 53 | response = { valid: boolean } -------------------------------------------------------------------------------- /client/redux/reducers/textFieldReducer.js: -------------------------------------------------------------------------------- 1 | import * as types from '../constants/actionTypes'; 2 | 3 | const initialState = { 4 | username: '', 5 | password: '', 6 | fullName: '', 7 | message: '', 8 | response: { valid: false }, 9 | validUser: undefined, 10 | validLogin: undefined, 11 | lat: '', 12 | long: '', 13 | }; 14 | 15 | const textFieldReducer = (state = initialState, action) => { 16 | 17 | const updateTextPerLetter = typeOfField => { 18 | let curStateVal = state[typeOfField]; 19 | if (action.payload.inputType === 'deleteContentBackward') 20 | curStateVal = state[typeOfField].slice(0, -1); 21 | else curStateVal = state[typeOfField] + action.payload.data; 22 | return curStateVal; 23 | }; 24 | 25 | switch (action?.type) { 26 | case types.UPDATE_LOCATION: 27 | const { lat, long } = action.payload; 28 | 29 | return { 30 | ...state, 31 | lat, 32 | long, 33 | }; 34 | 35 | // case types.USERNAME_CHANGE: 36 | // const newUserName = updateTextPerLetter('username'); 37 | // return { 38 | // ...state, 39 | // username: newUserName, 40 | // }; 41 | 42 | // case types.PASSWORD_CHANGE: 43 | // const newPassword = updateTextPerLetter('password'); 44 | // return { 45 | // ...state, 46 | // password: newPassword, 47 | // }; 48 | 49 | // case types.FULL_NAME_CHANGE: 50 | // const newFullName = updateTextPerLetter('fullName'); 51 | // return { 52 | // ...state, 53 | // fullName: newFullName, 54 | // }; 55 | 56 | // case types.RESET_FIELDS: 57 | // return { 58 | // ...state, 59 | // username: '', 60 | // password: '', 61 | // fullName: '', 62 | // }; 63 | 64 | case types.CREATE_ACCOUNT_SUBMIT: 65 | return { 66 | ...state, 67 | validUser: false, 68 | }; 69 | 70 | case types.LOGIN_SUBMIT: 71 | return { 72 | ...state, 73 | validLogin: false, 74 | }; 75 | 76 | default: 77 | return state; 78 | } 79 | }; 80 | 81 | export default textFieldReducer; 82 | -------------------------------------------------------------------------------- /__tests__/client/reducers/responsesReducer.test.js: -------------------------------------------------------------------------------- 1 | import responsesReducer from '../../../client/redux/reducers/responsesReducer'; 2 | 3 | describe('responsesReducer testing suite.', () => { 4 | const result = responsesReducer(); 5 | test('mode property should be string.', () => { 6 | expect(typeof result.mode === 'string').toBeTruthy(); 7 | }) 8 | 9 | test('signUpPost property should be an object with key valid that is a boolean.', () => { 10 | expect(typeof result.signUpPost === 'object').toBeTruthy(); 11 | expect(typeof result.signUpPost.valid === 'boolean').toBeTruthy(); 12 | }) 13 | 14 | test('loginGet property should be an object with key valid that is a boolean.', () => { 15 | expect(typeof result.loginGet === 'object').toBeTruthy(); 16 | expect(typeof result.loginGet.valid === 'boolean').toBeTruthy(); 17 | }) 18 | 19 | test('testMessages property should be an array of objects with appropriate keys and values.', () => { 20 | expect(result.testMessages instanceof Array).toBeTruthy(); 21 | for (let ind = 0; ind < result.testMessages.length; ind++) { 22 | expect(typeof result.testMessages[ind].username === 'string').toBeTruthy(); 23 | expect(typeof result.testMessages[ind].sciName === 'string').toBeTruthy(); 24 | expect(typeof result.testMessages[ind].timeStamp === 'string').toBeTruthy(); 25 | expect(typeof result.testMessages[ind].location === 'object').toBeTruthy(); 26 | expect(typeof result.testMessages[ind].location.area === 'string').toBeTruthy(); 27 | } 28 | }) 29 | 30 | test('testSeenBirds property should be an array of objects with appropriate keys and values.', () => { 31 | expect(result.testSeenBirds instanceof Array).toBeTruthy(); 32 | for (let ind = 0; ind < result.testSeenBirds.length; ind++) { 33 | expect(typeof result.testSeenBirds[ind].sciName === 'string').toBeTruthy(); 34 | expect(typeof result.testSeenBirds[ind].timeStamp === 'string').toBeTruthy(); 35 | } 36 | }) 37 | 38 | test('testLocalBirds property should be an array of objects with appropriate keys and values.', () => { 39 | expect(result.testLocalBirds instanceof Array).toBeTruthy(); 40 | for (let ind = 0; ind < result.testLocalBirds.length; ind++) { 41 | expect(typeof result.testLocalBirds[ind].sciName === 'string').toBeTruthy(); 42 | } 43 | }) 44 | }) -------------------------------------------------------------------------------- /client/react/components/FriendSitings.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import * as actions from '../../redux/actions/actions.js'; 4 | 5 | const mapStateToProps = state => ({ 6 | mode: state.responses.mode, 7 | testMessages: state.responses.testMessages, 8 | friendMessages: state.messages.friendMessages, 9 | }); 10 | 11 | const mapDispatchToProps = dispatch => ({ 12 | updateFriendMessagesActionCreator: payload => 13 | dispatch(actions.updateFriendMessagesActionCreator(payload)), 14 | }); 15 | 16 | class FriendSitings extends Component { 17 | constructor(props) { 18 | super(props); 19 | 20 | this.getFriendMessages = this.getFriendMessages.bind(this); 21 | } 22 | 23 | getFriendMessages() { 24 | const url = `http://localhost:3000/Friend/?username=${this.props.username}`; 25 | 26 | if (this.props.mode === 'dev') 27 | this.props.updateFriendMessagesActionCreator(this.props.testMessages); 28 | else if (this.props.mode === 'prod') { 29 | fetch(url, { 30 | method: 'GET', 31 | header: { 'Access-Control-Allow-Origin': ' * ', 'Content-Type': 'application/json' }, 32 | }) 33 | .then(data => data.json()) 34 | .then(data => this.props.updateFriendMessagesActionCreator(data.messages)) 35 | .catch(err => console.log(err)); 36 | } else console.log('Mode must be prod or dev in ./client/reducers/responsesReducer.js'); 37 | } 38 | 39 | componentDidMount() { 40 | this.getFriendMessages(); 41 | } 42 | 43 | render() { 44 | const display = []; 45 | 46 | if (this.props.friendMessages instanceof Array) { 47 | if (this.props.friendMessages.length === 0) 48 | display.push(

No friend messages available at this time.

); 49 | else { 50 | this.props.friendMessages.forEach((mess, ind) => { 51 | display.push( 52 |
53 |

{`${mess.username} saw a ${mess.sciBirdName} in ${mess.location.area} around ${mess.timeStamp}`}

57 |
58 | ); 59 | }); 60 | } 61 | } else display.push(

Error with friendMessages

); 62 | 63 | return ( 64 |
65 | {display} 66 |
67 | ); 68 | } 69 | } 70 | 71 | export default connect(mapStateToProps, mapDispatchToProps)(FriendSitings); 72 | -------------------------------------------------------------------------------- /client/react/components/CommunitySitings.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import * as actions from '../../redux/actions/actions.js'; 4 | 5 | const mapStateToProps = state => ({ 6 | mode: state.responses.mode, 7 | testMessages: state.responses.testMessages, 8 | communityMessages: state.messages.communityMessages, 9 | }); 10 | 11 | const mapDispatchToProps = dispatch => ({ 12 | updateCommunityMessagesActionCreator: payload => 13 | dispatch(actions.updateCommunityMessagesActionCreator(payload)), 14 | }); 15 | 16 | class CommunitySitings extends Component { 17 | constructor(props) { 18 | super(props); 19 | 20 | this.getCommunityMessages = this.getCommunityMessages.bind(this); 21 | } 22 | 23 | getCommunityMessages() { 24 | const url = `http://localhost:3000/community/?username=${this.props.username}&location=${this.props.location}`; 25 | 26 | if (this.props.mode === 'dev') 27 | this.props.updateCommunityMessagesActionCreator(this.props.testMessages); 28 | else if (this.props.mode === 'prod') { 29 | fetch(url, { 30 | method: 'GET', 31 | header: { 'Access-Control-Allow-Origin': ' * ', 'Content-Type': 'application/json' }, 32 | }) 33 | .then(data => data.json()) 34 | .then(data => this.props.updateCommunityMessagesActionCreator(data.messages)) 35 | .catch(err => console.log(err)); 36 | } else console.log('Mode must be prod or dev in ./client/reducers/responsesReducer.js'); 37 | } 38 | 39 | componentDidMount() { 40 | this.getCommunityMessages(); 41 | } 42 | 43 | render() { 44 | const display = []; 45 | if (this.props.communityMessages instanceof Array) { 46 | if (this.props.communityMessages.length === 0) 47 | display.push(

No community messages available at this time.

); 48 | else { 49 | this.props.communityMessages.forEach((mess, ind) => { 50 | display.push( 51 |
52 |

{`${mess.username} saw a ${mess.sciBirdName} in ${mess.location.area} around ${mess.timeStamp}`}

56 |
57 | ); 58 | }); 59 | } 60 | } else display.push(

Error with communityMessages

); 61 | 62 | return ( 63 |
64 | {display} 65 |
66 | ); 67 | } 68 | } 69 | 70 | export default connect(mapStateToProps, mapDispatchToProps)(CommunitySitings); 71 | -------------------------------------------------------------------------------- /client/react/components/NavBar.jsx: -------------------------------------------------------------------------------- 1 | // import path from 'path'; 2 | import React, { Component } from 'react'; 3 | 4 | import LogoIcon from '../../../assets/img/brdl-logo-2-b.png'; 5 | import LogoText from '../../../assets/img/brdl-logo-2-c.png'; 6 | 7 | class NavBar extends Component { 8 | constructor(props) { 9 | super(props); 10 | } 11 | 12 | render() { 13 | const display = []; 14 | 15 | if (this.props.currPage === 'signUp' || this.props.currPage === 'login') { 16 | display.push( 17 | 41 | ); 42 | } else { 43 | display.push( 44 | 62 | ); 63 | } 64 | return ( 65 |
66 |
67 | this.props.changePageActionCreator('profile')}> 68 | {/*

LOGO

*/} 69 | {/*
70 | 71 |
72 | */} 73 | 74 | 75 | this.props.changePageActionCreator('profile')}> 76 | {/*

BRDL

*/} 77 | 78 |
79 |
80 |
{display}
81 |
82 | ); 83 | } 84 | } 85 | 86 | export default NavBar; 87 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | const path = require('path'), 2 | express = require('express'), 3 | PORT = 3000, 4 | app = express(); 5 | 6 | const userController = require('./controllers/userController'); 7 | const geoController = require('./controllers/geoController'); 8 | const birdController = require('./controllers/birdController'); 9 | 10 | app.use(express.json()); // replaces body-parser 11 | app.use(express.urlencoded({ extended: true })); // Helps parse different data types 12 | 13 | // handle GET & POST requests to /gainAccess 14 | 15 | // Login route 16 | // middleware will return a boolean. 17 | // if false, res.send('Login credentials are invalid') 18 | // else, direct user to the profile page 19 | app.get('/gainAccess', userController.auth, (req, res) => { 20 | res.set('Access-Control-Allow-Origin', ' * '); 21 | res.set('Content-Type', 'application/json'); 22 | 23 | res.status(200).json(res.locals.auth); 24 | }); 25 | 26 | // Account creation 27 | // mw will return a boolean 28 | // if false, res.send('Account creation failed') 29 | // else, direct user to profile page 30 | app.post('/gainAccess', userController.create, (req, res) => { 31 | res.set('Access-Control-Allow-Origin', ' * '); 32 | res.set('Content-Type', 'application/json'); 33 | 34 | res.status(200).json(res.locals.auth); 35 | }); 36 | 37 | // User profile - get local birds in current area 38 | // client will send a GET request to /profile with { username: value, lat: value, long: value } 39 | // for 10 birds, mw will return { birds: [{sciName: "", locName: ""}, {...}]} 40 | app.get('/profile', birdController.nearby, (req, res) => { 41 | res.set('Access-Control-Allow-Origin', ' * '); 42 | res.set('Content-Type', 'application/json'); 43 | 44 | res.status(200).json(res.locals.nearby); 45 | }); 46 | 47 | app.post('/profile', birdController.seen, (req,res) => { 48 | res.set('Access-Control-Allow-Origin', ' * '); 49 | res.set('Content-Type', 'application/json'); 50 | 51 | res.status(200).json(res.locals.seen); 52 | }) 53 | 54 | // Local error handler (404/missing routes) 55 | app.use('*', (req, res) => { 56 | res.set('Access-Control-Allow-Origin', ' * '); 57 | res.set('Content-Type', 'application/json'); 58 | 59 | res.status(404).send('PAGE NOT FOUND!!!'); 60 | }); 61 | // Global error handler (middleware errors) 62 | app.use((err, req, res, next) => { 63 | const defaultErr = { 64 | log: 'Express error handler caught unknown middleware error', 65 | status: 500, 66 | message: { err: 'Express error handler caught unknown middleware error' }, 67 | }; 68 | const errorObj = Object.assign({}, defaultErr, err); 69 | console.log(errorObj.log); 70 | 71 | res.set('Access-Control-Allow-Origin', ' * '); 72 | res.set('Content-Type', 'application/json'); 73 | 74 | return res.status(errorObj.status).json(errorObj.message); 75 | }); 76 | 77 | app.listen(PORT, err => { 78 | if (err) console.log(err); 79 | else console.log('Server listening on PORT', PORT); 80 | }); 81 | 82 | module.exports = app; 83 | -------------------------------------------------------------------------------- /server/controllers/userController.js: -------------------------------------------------------------------------------- 1 | const { query } = require('express'); 2 | const db = require('../models/brdlModels'); 3 | 4 | const userController = {}; 5 | 6 | // client will provide username and password in the req.query 7 | // we will query the db using just the username and get the password from the db. if the db provided password matches client provided password, set res.locals.auth = true 8 | // else set res.locals.auth 9 | userController.auth = async (req, res, next) => { 10 | const { username: clientUsername, password: clientPassword } = req.query; 11 | try { 12 | const queryString = 'SELECT * FROM Users WHERE username=$1'; 13 | const queryResult = await db.query(queryString, [clientUsername]); 14 | if (!queryResult.rows.length || queryResult.rows[0].password !== clientPassword) { 15 | console.log(`Auth failed using username: ${clientUsername} and password: ${clientPassword}`); 16 | res.locals.auth = { valid: false }; 17 | return next(); 18 | } else { 19 | res.locals.auth = { valid: true, fullName: queryResult.rows[0].name }; 20 | console.log(`Auth success using username: ${clientUsername} and password: ${clientPassword}`); 21 | return next(); 22 | } 23 | } catch (err) { 24 | return next({ 25 | log: `Express error handler caught in userController.auth: ${err.message}`, 26 | status: 500, 27 | message: { err: 'Express error handler caught in userController.auth' }, 28 | }); 29 | } 30 | }; 31 | 32 | // client will provide a unique username and a password in req.query 33 | // we will query database with both the username and password. If the db does not contain username, it will store the username and password and set res.locals.auth = true 34 | // else set res.locals.auth to false if username already exists 35 | userController.create = async (req, res, next) => { 36 | console.log('in usercontroller create'); 37 | const { fullName, username: clientUsername, password: clientPassword } = req.query; 38 | try { 39 | const queryCheckString = 'SELECT * FROM Users WHERE username=$1'; 40 | const queryCheckResult = await db.query(queryCheckString, [clientUsername]); 41 | if (queryCheckResult.rows.length > 0) { 42 | res.locals.auth = { valid: false }; 43 | console.log('username already exist'); 44 | return next(); 45 | } else { 46 | const queryString = 'INSERT INTO Users (name, username, password) VALUES ($1, $2, $3)'; 47 | const queryResult = await db.query(queryString, [fullName, clientUsername, clientPassword]); 48 | res.locals.auth = { valid: true }; 49 | console.log('account succcessfully made'); 50 | console.log(queryResult); 51 | return next(); 52 | } 53 | } catch (err) { 54 | return next({ 55 | log: `Express error handler caught in userController.create: ${err.message}`, 56 | status: 500, 57 | message: { err: 'Express error handler caught in userController.create' }, 58 | }); 59 | } 60 | }; 61 | 62 | module.exports = userController; 63 | -------------------------------------------------------------------------------- /client/react/components/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import * as actions from '../../redux/actions/actions.js'; 4 | import SignUp from './SignUp.jsx'; 5 | import Login from './Login.jsx'; 6 | import Navbar from './NavBar.jsx'; 7 | import CommunityContainer from '../containers/CommunityContainer.jsx'; 8 | import ProfileContainer from '../containers/ProfileContainer.jsx'; 9 | 10 | const mapStateToProps = state => ({ page: state.navigation.page }); 11 | 12 | const mapDispatchToProps = dispatch => ({ 13 | resetFieldsActionCreator: () => dispatch(actions.resetFieldsActionCreator()), 14 | changePageActionCreator: payload => dispatch(actions.changePageActionCreator(payload)), // Replaces the four below it 15 | // changeToLoginPageActionCreator: () => dispatch(actions.changeToLoginPageActionCreator()), 16 | // changeToSignUpPageActionCreator: () => dispatch(actions.changeToSignUpPageActionCreator()), 17 | // changeToCommunityPageActionCreator: () => dispatch(actions.changeToCommunityPageActionCreator()), 18 | // changeToProfilePageActionCreator: () => dispatch(actions.changeToProfilePageActionCreator()), 19 | }); 20 | 21 | class App extends Component { 22 | constructor(props) { 23 | super(props); 24 | } 25 | 26 | render() { 27 | // const display = [

brdl

]; 28 | 29 | const display = []; 30 | 31 | if (this.props.page === 'signUp') { 32 | display.push(); 33 | // display.push(

Sign Up

); 34 | // display.push() 35 | } else if (this.props.page === 'login') { 36 | display.push(); 37 | // display.push(

Login

); 38 | // display.push() 39 | } else if (this.props.page === 'community') display.push(); 40 | else if (this.props.page === 'profile') display.push(); 41 | 42 | return ( 43 |
44 | 55 | {display} 56 | {/*
57 | 60 | 63 |
*/} 64 |
65 | ); 66 | } 67 | } 68 | 69 | export default connect(mapStateToProps, mapDispatchToProps)(App); 70 | -------------------------------------------------------------------------------- /client/react/components/Login.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import * as actions from '../../redux/actions/actions.js'; 4 | 5 | /* 6 | loginGet: { 7 | valid: true, 8 | }, 9 | */ 10 | 11 | const mapStateToProps = state => ({ 12 | loginGet: state.responses.loginGet, 13 | username: state.textField.username, 14 | password: state.textField.password, 15 | validLogin: state.textField.validLogin, 16 | mode: state.responses.mode, 17 | }); 18 | 19 | const mapDispatchToProps = dispatch => ({ 20 | changeToProfilePageActionCreator: () => dispatch(actions.changeToProfilePageActionCreator()), 21 | usernameChangeActionCreator: () => dispatch(actions.usernameChangeActionCreator(event)), 22 | passwordChangeActionCreator: () => dispatch(actions.passwordChangeActionCreator(event)), 23 | loginSubmitActionCreator: (e, mode, serverRes) => 24 | dispatch(actions.loginSubmitActionCreator(e, mode, serverRes)), 25 | }); 26 | 27 | class Login extends Component { 28 | constructor(props) { 29 | super(props); 30 | } 31 | 32 | handleAccountSubmit(e, mode, serverRes) { 33 | e.preventDefault(); 34 | const username = document.getElementById('username').value; 35 | const password = document.getElementById('password').value; 36 | document.getElementById('username').value = ''; 37 | document.getElementById('password').value = ''; 38 | 39 | if (this.props.mode === 'dev') { 40 | // this.props.loginGet.valid = false; 41 | if (this.props.loginGet.valid) this.props.changeToProfilePageActionCreator(); 42 | else this.props.loginSubmitActionCreator(); 43 | } else { 44 | // queryRes = actual server query 45 | const url = `http://localhost:3000/gainAccess/?username=${username}&password=${password}`; 46 | const options = { 47 | method: 'GET', 48 | header: { 49 | 'Access-Control-Allow-Origin': ' * ', 50 | 'Content-Type': 'application/json', 51 | Accept: 'application/json', 52 | }, 53 | }; 54 | fetch(url, options) 55 | .then(res => res.json()) 56 | .then(data => { 57 | if (data.valid) this.props.changeToProfilePageActionCreator(); 58 | else this.props.loginSubmitActionCreator(); 59 | }); 60 | } 61 | } 62 | 63 | render() { 64 | return ( 65 |
66 |
67 |

Already have an account?

68 |

Sign in and get brdlng!

69 |
70 | 71 |
this.handleAccountSubmit(e)} data-testid="form"> 72 | 82 | 92 | 93 | 96 | {this.props.validLogin === false ? ( 97 |

Invalid username or password

98 | ) : ( 99 |

100 | )} 101 |
102 |
103 | ); 104 | } 105 | } 106 | 107 | export default connect(mapStateToProps, mapDispatchToProps)(Login); 108 | -------------------------------------------------------------------------------- /client/style.css: -------------------------------------------------------------------------------- 1 | /* 2 | Colors 3 | 4 | hero 1: #f7bf5a 5 | hero 2: #261803 6 | 7 | shade 1: deac51 8 | shade 2: c69948 9 | shad 3: ad863f 10 | 11 | shade 5: 7c602d 12 | shade 6: 634c24 13 | shae 7: 312612 14 | 15 | tint 1: f8c56b 16 | tint 2: f9cc7b 17 | tint 3: f9d28c 18 | tint 4: fad99c 19 | tint 5: fbdfad 20 | 21 | tint 8: fdf2de 22 | tint 9: fef9ef 23 | 24 | */ 25 | 26 | * { 27 | margin: 0; 28 | padding: 0; 29 | box-sizing: border-box; 30 | } 31 | 32 | .dev { 33 | position: absolute; 34 | bottom: 0; 35 | left: 0; 36 | } 37 | 38 | body { 39 | background-color: #fdf2de; 40 | color: #261803#261803; 41 | font-family: 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, 42 | Verdana, sans-serif; 43 | /* padding: 0; */ 44 | } 45 | 46 | .nav-bar-container { 47 | display: flex; 48 | justify-content: space-between; 49 | padding: 16px 20px; 50 | margin-bottom: 80px; 51 | background-color: #f7bf5a; 52 | position: sticky; 53 | top: 0; 54 | align-items: center; 55 | } 56 | 57 | .nav-logo { 58 | width: 50px; 59 | height: 50px; 60 | } 61 | 62 | .nav-logo-container { 63 | font-size: 24px; 64 | font-weight: 600; 65 | display: flex; 66 | } 67 | 68 | .nav-nav-container ul { 69 | display: flex; 70 | list-style: none; 71 | gap: 13px; 72 | font-weight: 600; 73 | font-size: 16px; 74 | } 75 | 76 | .nav-bar-container a:link, 77 | .nav-bar-container a:visited { 78 | text-decoration: none; 79 | color: inherit; 80 | padding: 8px 16px; 81 | border-radius: 10px; 82 | transition: all, 0.2s; 83 | } 84 | 85 | .nav-bar-container a:hover, 86 | .nav-bar-container a:active { 87 | background-color: #312612; 88 | color: #eee; 89 | } 90 | 91 | .signup-container, 92 | .login-container, 93 | .component-container { 94 | display: flex; 95 | flex-direction: column; 96 | gap: 0px; 97 | padding: 50px; 98 | width: 675px; 99 | margin: 0 auto; 100 | margin-top: 100px; 101 | border: #261803 solid 3px; 102 | border-radius: 10px; 103 | box-shadow: 3px 3px 8px 1px #2618033b; 104 | } 105 | 106 | header { 107 | margin-bottom: 60px; 108 | } 109 | 110 | h1 { 111 | /* font-family: 'Bebas Neue'; */ 112 | margin-top: 0; 113 | margin-bottom: 32px; 114 | } 115 | 116 | .profile-header { 117 | margin-bottom: 20px; 118 | } 119 | 120 | .seen-birds-header { 121 | margin-bottom: 20px; 122 | } 123 | 124 | .bird-row, 125 | .bird-row-seen { 126 | border: 2px solid #261803; 127 | border-radius: 10px; 128 | } 129 | 130 | .bird-row-seen { 131 | background-color: #f7bf5a; 132 | /* color: #eee; */ 133 | padding: 10px 15px; 134 | } 135 | 136 | .bird-info { 137 | padding: 8px 16px; 138 | } 139 | 140 | .friend-sighting-heading { 141 | margin-top: 50px; 142 | } 143 | 144 | form, 145 | .component-sub-container { 146 | padding: 50px; 147 | 148 | display: flex; 149 | flex-direction: column; 150 | gap: 30px; 151 | background-color: #fbdfad; 152 | border-radius: 10px; 153 | } 154 | 155 | form label { 156 | display: flex; 157 | align-items: center; 158 | gap: 10px; 159 | } 160 | 161 | input { 162 | font-size: 18px; 163 | padding: 4px 8px; 164 | border-radius: 10px; 165 | color: #261803; 166 | box-shadow: 0 0 0 0; 167 | border: none; 168 | } 169 | 170 | input:focus { 171 | outline: 2px solid #261803; 172 | } 173 | 174 | .create-account-btn, 175 | .login-btn, 176 | .btn { 177 | background-color: #261803; 178 | color: #eee; 179 | display: inline-block; 180 | width: 50%; 181 | margin: 0 auto; 182 | 183 | border: none; 184 | padding-top: 8px; 185 | padding-bottom: 8px; 186 | font-size: 18px; 187 | border-radius: 10px; 188 | transition: all, 0.2s; 189 | font-weight: 600; 190 | } 191 | 192 | .create-account-btn, 193 | .login-btn { 194 | margin-top: 20px; 195 | } 196 | 197 | .btn { 198 | margin-top: 0px; 199 | margin-bottom: 20px; 200 | } 201 | 202 | .create-account-btn:hover, 203 | .login-btn:hover, 204 | .btn:hover { 205 | cursor: pointer; 206 | background-color: #f7bf5a; 207 | color: #261803; 208 | } 209 | 210 | .validation-msg { 211 | text-align: center; 212 | } 213 | 214 | .hidden { 215 | display: none; 216 | } 217 | -------------------------------------------------------------------------------- /__tests__/client/actions.test.js: -------------------------------------------------------------------------------- 1 | import * as actions from '../../client/redux/actions/actions.js'; 2 | import * as types from '../../client/redux/constants/actionTypes'; 3 | 4 | describe('Actions and action types testing suite.', () => { 5 | const pL = 'test123'; 6 | const payL = { e: 'test', mode: '123', serverRes: '456' } 7 | 8 | test('Should generate change page action.', () => { 9 | const result = actions.changePageActionCreator(pL); 10 | expect(result.type).toBe(types.CHANGE_PAGE); 11 | expect(result.payload).toBe(pL); 12 | }) 13 | 14 | test('Should generate change to login page action.', () => { 15 | const result = actions.changeToLoginPageActionCreator(pL); 16 | expect(result.type).toBe(types.LOGIN); 17 | expect(result.payload).toBe(undefined); 18 | }) 19 | 20 | test('Should generate change to sign up action.', () => { 21 | const result = actions.changeToSignUpPageActionCreator(pL); 22 | expect(result.type).toBe(types.SIGN_UP); 23 | expect(result.payload).toBe(undefined); 24 | }) 25 | 26 | test('Should generate change to community action.', () => { 27 | const result = actions.changeToCommunityPageActionCreator(pL); 28 | expect(result.type).toBe(types.COMMUNITY); 29 | expect(result.payload).toBe(undefined); 30 | }) 31 | 32 | test('Should generate change to profile action.', () => { 33 | const result = actions.changeToProfilePageActionCreator(pL); 34 | expect(result.type).toBe(types.PROFILE); 35 | expect(result.payload).toBe(undefined); 36 | }) 37 | 38 | test('Should generate username change action.', () => { 39 | const result = actions.usernameChangeActionCreator(pL); 40 | expect(result.type).toBe(types.USERNAME_CHANGE); 41 | expect(result.payload).toBe(pL); 42 | }) 43 | 44 | test('Should generate password change action.', () => { 45 | const result = actions.passwordChangeActionCreator(pL); 46 | expect(result.type).toBe(types.PASSWORD_CHANGE); 47 | expect(result.payload).toBe(pL); 48 | }) 49 | 50 | test('Should generate full name change action.', () => { 51 | const result = actions.fullNameChangeActionCreator(pL); 52 | expect(result.type).toBe(types.FULL_NAME_CHANGE); 53 | expect(result.payload).toBe(pL); 54 | }) 55 | 56 | test('Should generate reset fields action.', () => { 57 | const result = actions.resetFieldsActionCreator(pL); 58 | expect(result.type).toBe(types.RESET_FIELDS); 59 | expect(result.payload).toBe(undefined); 60 | }) 61 | 62 | test('Should generate create account submit action.', () => { 63 | const result = actions.createAccountSubmitActionCreator(payL.e, payL.mode, payL.serverRes); 64 | expect(result.type).toBe(types.CREATE_ACCOUNT_SUBMIT); 65 | expect(result.payload).toEqual(payL); 66 | }) 67 | 68 | test('Should generate login submit action.', () => { 69 | const result = actions.loginSubmitActionCreator(payL.e, payL.mode, payL.serverRes); 70 | expect(result.type).toBe(types.LOGIN_SUBMIT); 71 | expect(result.payload).toEqual(payL); 72 | }) 73 | 74 | test('Should generate update seen birds action.', () => { 75 | const result = actions.updateSeenBirdsActionCreator(pL); 76 | expect(result.type).toBe(types.UPDATE_SEEN_BIRDS); 77 | expect(result.payload).toEqual(pL); 78 | }) 79 | 80 | test('Should generate update local birds action.', () => { 81 | const result = actions.updateLocalBirdsActionCreator(pL); 82 | expect(result.type).toBe(types.UPDATE_LOCAL_BIRDS); 83 | expect(result.payload).toEqual(pL); 84 | }) 85 | 86 | test('Should generate update friend messages action.', () => { 87 | const result = actions.updateFriendMessagesActionCreator(pL); 88 | expect(result.type).toBe(types.UPDATE_FRIEND_MESSAGES); 89 | expect(result.payload).toEqual(pL); 90 | }) 91 | 92 | test('Should generate update community messages action.', () => { 93 | const result = actions.updateCommunityMessagesActionCreator(pL); 94 | expect(result.type).toBe(types.UPDATE_COMMUNITY_MESSAGES); 95 | expect(result.payload).toEqual(pL); 96 | }) 97 | 98 | test('Should generate update location messages action.', () => { 99 | const result = actions.updateLocationActionCreator(pL); 100 | expect(result.type).toBe(types.UPDATE_LOCATION); 101 | expect(result.payload).toEqual(pL); 102 | }) 103 | }) -------------------------------------------------------------------------------- /client/react/components/SignUp.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import * as actions from '../../redux/actions/actions.js'; 4 | // import birdies from '../../../assets/img/brdl-logo-6-a.png'; 5 | 6 | const displayMessage = []; 7 | 8 | const mapStateToProps = state => ({ 9 | username: state.textField.username, 10 | password: state.textField.password, 11 | fullName: state.textField.fullName, 12 | validUser: state.textField.validUser, 13 | mode: state.responses.mode, 14 | signUpPost: state.responses.signUpPost, 15 | }); 16 | 17 | const mapDispatchToProps = dispatch => ({ 18 | usernameChangeActionCreator: () => dispatch(actions.usernameChangeActionCreator(event)), 19 | passwordChangeActionCreator: () => dispatch(actions.passwordChangeActionCreator(event)), 20 | fullNameChangeActionCreator: () => dispatch(actions.fullNameChangeActionCreator(event)), 21 | createAccountSubmitActionCreator: (e, mode, serverRes) => 22 | dispatch(actions.createAccountSubmitActionCreator(e, mode, serverRes)), // remove server res argument when not needed 23 | changeToLoginPageActionCreator: () => dispatch(actions.changeToLoginPageActionCreator()), 24 | changeToProfilePageActionCreator: () => dispatch(actions.changeToProfilePageActionCreator()), 25 | }); 26 | 27 | class SignUp extends Component { 28 | constructor(props) { 29 | super(props); 30 | this.handleAccountSubmit = this.handleAccountSubmit.bind(this); 31 | } 32 | 33 | handleAccountSubmit(e, mode, serverRes) { 34 | e.preventDefault(); 35 | // console.log(e, { mode }, { serverRes }); 36 | const username = document.getElementById('username').value; 37 | const password = document.getElementById('password').value; 38 | const fullName = document.getElementById('full-name').value; 39 | document.getElementById('username').value = ''; 40 | document.getElementById('password').value = ''; 41 | 42 | if (this.props.mode === 'dev') { 43 | // this.props.signUpPost.valid = false; 44 | if (this.props.signUpPost.valid) this.props.changeToProfilePageActionCreator(); 45 | else this.props.createAccountSubmitActionCreator(); 46 | } else { 47 | // queryRes = actual server query 48 | const url = `http://localhost:3000/gainAccess/?username=${username}&password=${password}&fullName=${fullName}`; 49 | const options = { 50 | method: 'POST', 51 | header: { 'Access-Control-Allow-Origin': ' * ', 'Content-Type': 'application/json' }, 52 | }; 53 | fetch(url, options) 54 | .then(res => res.json()) 55 | .then(data => { 56 | if (data.valid) this.props.changeToProfilePageActionCreator(); 57 | else this.props.createAccountSubmitActionCreator(); 58 | }); 59 | } 60 | } 61 | 62 | render() { 63 | return ( 64 |
65 |
66 |

New to brd wtchng?

67 |

Create a brdl account and get started today!

68 |
69 | 70 |
this.handleAccountSubmit(e)} data-testid="form"> 71 | 81 | 91 | 101 | 104 | {this.props.validUser === false ? ( 105 |

Username is already taken

106 | ) : ( 107 |

108 | )} 109 |
110 |
111 | ); 112 | } 113 | } 114 | 115 | export default connect(mapStateToProps, mapDispatchToProps)(SignUp); 116 | -------------------------------------------------------------------------------- /server/controllers/birdController.js: -------------------------------------------------------------------------------- 1 | const db = require('../models/brdlModels'); 2 | const axios = require('axios'); 3 | const tokens = require('../tokens/tokens'); 4 | 5 | const birdController = {}; 6 | 7 | // GET -- client will provide { username: value, lat: value, long: value } in req.query 8 | // respond with 10 birds in area { birds: [ { comBirdName: value, sciBirdName: value }, {...}, ... ], 9 | 10 | birdController.nearby = async (req, res, next) => { 11 | const { username, lat, long } = req.query; 12 | // lat/long must be up to 2 decimal points 13 | try { 14 | const apiResponse = await axios.get(`https://api.ebird.org/v2/data/obs/geo/recent?lat=${lat}&lng=${long}&maxResults=5`, { 15 | headers: { "x-ebirdapitoken": tokens.eBirdToken }, 16 | }) 17 | const newBirdList = apiResponse.data.map(bird => ({ 18 | sciName: bird.sciName, 19 | comName: bird.comName, 20 | locName: bird.locName 21 | })) 22 | const seenQuery = 'SELECT scientific_name, time_stamp FROM "public"."seen_birds" WHERE username=$1'; 23 | const seenResult = await db.query(seenQuery, [username]); 24 | res.locals.nearby = { birds: newBirdList, seenBirds: seenResult.rows }; 25 | // INSERTING ONE BY ONE 26 | const queryString = `INSERT INTO Birds (scientific_name, common_name) VALUES ($1, $2) ON CONFLICT (scientific_name) DO NOTHING`; 27 | newBirdList.forEach(async function (bird) { 28 | try { await db.query(queryString, [bird.sciName, bird.comName]) } 29 | catch (err) { 30 | console.log('Cannot insert bird into Birds table'); 31 | console.log(err); 32 | } 33 | }) 34 | 35 | // INSERTING ALL AT ONCE IGNORE DUPLICATES (error- if bird name contains single quote, need to add escape character to ignore) 36 | // const queryStart = 'INSERT INTO Birds (scientific_name, common_name) VALUES '; 37 | // const queryMid = newBirdList.map(bird => { 38 | // return `("${bird.sciName}", "${bird.comName}")` 39 | // }).join(', '); 40 | // const queryEnd = ' ON CONFLICT (scientific_name) DO NOTHING'; 41 | // const queryString = queryStart + queryMid + queryEnd; 42 | // db.query(queryString) 43 | // .then(() => console.log('successfully added')); 44 | 45 | return next(); 46 | } catch (err) { 47 | return next({ 48 | log: `Express error handler caught in birdController.nearby: ${err.message}`, 49 | status: 500, 50 | message: { err: 'Express error handler caught in birdController.nearby' } 51 | }) 52 | } 53 | }; 54 | 55 | // POST -- when user clicks on a bird they have seen in the area, client will provide username, lat/long, timeStamp, commBirdName, sciBirdName 56 | // querey database to insert bird into the database 57 | birdController.seen = async (req, res, next) => { 58 | try { 59 | const { username, sciBirdName } = req.query; 60 | sciBirdName.split('_').join(' '); 61 | // check if the bird exists in birds table, if it doesn't insert into birds table 62 | const queryString = "SELECT * FROM Birds WHERE scientific_name=$1" 63 | const queryResult = await db.query(queryString, [sciBirdName]); 64 | if (!queryResult.rows.length) { 65 | const queryInsert = `INSERT INTO Birds (scientific_name, common_name) VALUES ($1, 'unknown')` 66 | await db.query(queryInsert, [sciBirdName]); 67 | console.log('BIRD ADDED TO TABLE') 68 | } 69 | // insert into the seen_bird table username, bird see, and time seen 70 | const querySeen = `INSERT INTO seen_birds (username, scientific_name, time_stamp) VALUES ($1, $2, CURRENT_TIMESTAMP) RETURNING time_stamp` 71 | const seenResult = await db.query(querySeen, [username, sciBirdName]); 72 | const timeSeen = seenResult.rows[0].time_stamp; 73 | res.locals.seen = { sciName: sciBirdName, timeStamp: timeSeen } 74 | return next() 75 | } catch (err) { 76 | return next({ 77 | log: `Express error handler caught in birdController.seen: ${err.message}`, 78 | status: 500, 79 | message: { err: 'Express error handler caught in birdController.seen' } 80 | }); 81 | } 82 | }; 83 | 84 | // const queryString = `INSERT INTO Birds (scientific_name, common_name) VALUES ($1, $2)` 85 | // const queryResult = await db.query(queryString, [newBirdList.sciName, ]); 86 | // may need to insert 10 birds into db here, but will need to ensure scientific_name type is set to UNIQUE 87 | // 88 | 89 | // DELETE -- when a user "unclicks" a bird they have seen, client will provide username, sciBirdName 90 | // we will respond with T/F if bird was successfully deleted from database 91 | 92 | module.exports = birdController; 93 | 94 | -------------------------------------------------------------------------------- /client/react/components/UserStats.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import * as actions from '../../redux/actions/actions.js'; 4 | 5 | const mapStateToProps = state => ({ 6 | mode: state.responses.mode, 7 | username: state.textField.username, 8 | seenBirds: state.birds.seenBirds, 9 | localBirds: state.birds.localBirds, 10 | lat: state.textField.lat, 11 | long: state.textField.long, 12 | testSeenBirds: state.responses.testSeenBirds, 13 | testLocalBirds: state.responses.testLocalBirds, 14 | fullName: state.textField.fullName, 15 | }); 16 | 17 | const mapDispatchToProps = dispatch => ({ 18 | updateSeenBirdsActionCreator: payload => dispatch(actions.updateSeenBirdsActionCreator(payload)), 19 | updateLocalBirdsActionCreator: payload => dispatch(actions.updateLocalBirdsActionCreator(payload)), 20 | updateLocationActionCreator: payload => dispatch(actions.updateLocationActionCreator(payload)), 21 | }); 22 | 23 | class UserStats extends Component { 24 | constructor(props) { 25 | super(props); 26 | 27 | this.getBirds = this.getBirds.bind(this); 28 | this.newSeenBird = this.newSeenBird.bind(this); 29 | } 30 | 31 | newSeenBird(bird) { 32 | if (this.props.mode === 'dev') { 33 | this.props.seenBirds.push({ sciName: bird, timeStamp: '5pm' }); 34 | this.props.updateSeenBirdsActionCreator(this.props.seenBirds.slice()); 35 | } else if (this.props.mode === 'prod') { 36 | bird = bird.split(' ').join('_'); 37 | const url = `http://localhost:3000/profile?username=${this.props.username}&lat=${this.props.lat}&long=${this.props.long}&sciBirdName=${bird} `; 38 | 39 | fetch(url, { 40 | method: 'POST', 41 | header: { 'Access-Control-Allow-Origin': ' * ', 'Content-Type': 'application/json' }, 42 | }) 43 | .then(data => data.json()) 44 | .then(data => { 45 | if ('sciName' in data) { 46 | this.props.seenBirds.push({ sciName: data.sciName, timeStamp: data.timeStamp }); 47 | this.props.updateSeenBirdsActionCreator(this.props.seenBirds.slice()); 48 | } else console.log('Failed to update on the back end'); 49 | }) 50 | .catch(err => console.log(err)); 51 | } else console.log('Mode must be prod or dev in ./client/reducers/responsesReducer.js'); 52 | } 53 | 54 | getBirds(locInfo) { 55 | if (this.props.mode === 'dev') { 56 | this.props.updateSeenBirdsActionCreator(this.props.testSeenBirds); 57 | this.props.updateLocalBirdsActionCreator(this.props.testLocalBirds); 58 | } else if (this.props.mode === 'prod') { 59 | const url = `http://localhost:3000/profile?username=${this.props.username}&lat=${this.props.lat}&long=${this.props.long}`; 60 | 61 | fetch(url, { 62 | method: 'GET', 63 | header: { 'Access-Control-Allow-Origin': ' * ', 'Content-Type': 'application/json' }, 64 | }) 65 | .then(data => data.json()) 66 | .then(data => { 67 | data.seenBirds.forEach(bird => bird.sciName = bird.scientific_name); 68 | if ('seenBirds' in data) this.props.updateSeenBirdsActionCreator(data.seenBirds); 69 | this.props.updateLocalBirdsActionCreator(data.birds); 70 | // this.getBirdImages(data.birds); 71 | }) 72 | .catch(err => console.log(err)); 73 | } else console.log('Mode must be prod or dev in ./client/reducers/responsesReducer.js'); 74 | } 75 | 76 | componentDidMount() { 77 | navigator.geolocation?.getCurrentPosition(loc => { 78 | const lat = String(Math.floor(loc.coords.latitude * 100) / 100), 79 | long = String(Math.floor(loc.coords.longitude * 100) / 100), 80 | locInfo = {}; 81 | 82 | locInfo.lat = lat; 83 | locInfo.long = long; 84 | 85 | this.props.updateLocationActionCreator(locInfo); 86 | this.getBirds(); 87 | }); 88 | } 89 | 90 | render() { 91 | const display = []; 92 | if (this.props.localBirds instanceof Array) { 93 | const totalSeenBirds = this.props.seenBirds.length, 94 | totalBirdsInArea = this.props.localBirds.length, 95 | seenBirdNames = this.props.seenBirds.reduce((acc, curr) => { 96 | if ('sciName' in curr) curr.sciName = curr.sciName.split('_').join(' ') 97 | acc[curr.sciName] = true; 98 | return acc; 99 | }, {}); 100 | let seenBirdsInThisArea = 0; 101 | 102 | this.props.localBirds.forEach((bird, ind) => { 103 | let seen = 'Has not been seen.'; 104 | const birdSeen = bird.sciName in seenBirdNames; 105 | if (birdSeen) { 106 | seenBirdsInThisArea++; 107 | seen = 'Has been seen.'; 108 | display.push( 109 |
110 |

{`${bird.sciName} is in the area. ${seen}`}

114 |
115 | ); 116 | } else { 117 | display.push( 118 |
119 |

{`${bird.sciName} is in the area. ${seen}`}

123 |
124 | ); 125 | } 126 | if (!birdSeen) 127 | display.push( 128 | 131 | ); 132 | }); 133 | 134 | display.unshift( 135 |

{`You have seen ${totalSeenBirds}.\nYou have seen ${seenBirdsInThisArea} out of ${totalBirdsInArea} in the area`}

139 | ); 140 | } else display.push(

Error with localBirds

); 141 | 142 | return ( 143 |
144 | {display} 145 |
146 | ); 147 | } 148 | } 149 | 150 | export default connect(mapStateToProps, mapDispatchToProps)(UserStats); 151 | --------------------------------------------------------------------------------