├── .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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------