├── Readme.md ├── .gitignore ├── client ├── actions │ ├── actionTypes.js │ └── actions.js ├── index.html ├── store.js ├── reducers │ ├── index.js │ └── reducer.js ├── components │ ├── MakeAD.jsx │ ├── Favorites.jsx │ ├── Navbar.jsx │ ├── PreviousDonations.jsx │ ├── SearchResults.jsx │ ├── LoginBox.jsx │ ├── Card.jsx │ ├── SignUpBox.jsx │ ├── PrevDonCard.jsx │ └── Form.jsx ├── containers │ ├── Main.jsx │ └── Login.jsx ├── app.jsx └── styles.css ├── server ├── start.js ├── routes │ ├── main.js │ └── user.js ├── models │ └── userModel.js ├── server.js └── controllers │ ├── mainController.js │ └── userController.js ├── .babelrc ├── __test__ ├── client │ ├── actions.test.js │ └── reducer.test.js └── server │ ├── controllers │ ├── userController.test.js │ └── mainController.test.js │ └── endpoints.test.js ├── .eslintrc.json ├── webpack.config.js └── package.json /Readme.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .env -------------------------------------------------------------------------------- /client/actions/actionTypes.js: -------------------------------------------------------------------------------- 1 | // export const SAVE_USER = 'SAVE_USER'; -------------------------------------------------------------------------------- /server/start.js: -------------------------------------------------------------------------------- 1 | const app = require('./server'); 2 | 3 | const PORT = 3000; 4 | app.listen(PORT, () => { 5 | console.log(`Server listening on port: ${PORT}...`); 6 | }); 7 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-react" 5 | ], 6 | "plugins": [ 7 | "@babel/plugin-proposal-class-properties" 8 | ] 9 | } -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Aidwell 6 | 7 | 8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /client/actions/actions.js: -------------------------------------------------------------------------------- 1 | export const GET_CHARITIES = (res) => { 2 | return({ type: 'GET_CHARITIES', payload: res}) 3 | } 4 | 5 | export const SAVE_USER = (userInfo) => { 6 | return({type: 'SAVE_USER', payload: userInfo}) 7 | } -------------------------------------------------------------------------------- /client/store.js: -------------------------------------------------------------------------------- 1 | import { applyMiddleware, createStore } from 'redux'; 2 | import reducers from './reducers/index'; 3 | import thunk from 'redux-thunk'; 4 | 5 | const store = createStore( 6 | reducers, 7 | applyMiddleware(thunk) 8 | ); 9 | 10 | export default store; -------------------------------------------------------------------------------- /client/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import reducer from './reducer'; 3 | 4 | // combine reducers 5 | const reducers = combineReducers({ 6 | // if we had other reducers, they would go here 7 | state: reducer 8 | }); 9 | 10 | // make the combined reducers available for import 11 | export default reducers; -------------------------------------------------------------------------------- /client/components/MakeAD.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | 4 | class MakeAD extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | } 8 | render(){ 9 |
10 | In MakeAD 11 |
12 | } 13 | } 14 | 15 | export default MakeAD 16 | -------------------------------------------------------------------------------- /server/routes/main.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const mainController = require('../controllers/mainController'); 3 | 4 | const router = express.Router(); 5 | 6 | router.put( 7 | '/findCharities', 8 | mainController.buildQuery, 9 | mainController.getCharities, 10 | mainController.processCharities, 11 | (req, res) => res.status(200).send(res.locals.parsed) 12 | ); 13 | 14 | module.exports = router; 15 | -------------------------------------------------------------------------------- /client/reducers/reducer.js: -------------------------------------------------------------------------------- 1 | const initialState = { 2 | charities: [], 3 | user: {} 4 | }; 5 | 6 | const reducer = (state = initialState, action) => { 7 | switch (action?.type) { 8 | case "GET_CHARITIES": 9 | return { ...state, charities: action.payload }; 10 | case 'SAVE_USER': 11 | const newState = { ...state }; 12 | newState.user = action.payload; 13 | return newState; 14 | default: { 15 | return state; 16 | } 17 | } 18 | }; 19 | export default reducer; 20 | -------------------------------------------------------------------------------- /__test__/client/actions.test.js: -------------------------------------------------------------------------------- 1 | import * as actions from '../../client/actions/actions'; 2 | 3 | describe('Action creator tests', () => { 4 | it('Should create a GET_CHARITIES action', () => { 5 | const action = actions.GET_CHARITIES('payload'); 6 | expect(action.type).toBe('GET_CHARITIES'); 7 | expect(action.payload).toBe('payload'); 8 | }) 9 | 10 | it('Should create a SAVE_USER action', () => { 11 | const action = actions.SAVE_USER('payload'); 12 | expect(action.type).toBe('SAVE_USER'); 13 | expect(action.payload).toBe('payload'); 14 | }) 15 | }) -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jest": true 4 | }, 5 | "extends": [ 6 | "airbnb", 7 | "prettier", 8 | "plugin:node/recommended" 9 | ], 10 | "plugins": [ 11 | "prettier" 12 | ], 13 | "parserOptions": { 14 | "ecmaVersion": 2020, 15 | "allowImportExportEverywhere": true 16 | }, 17 | "rules": { 18 | "prettier/prettier": [ 19 | "warn", 20 | { 21 | "singleQuote": true, 22 | "semi": true, 23 | "printWidth": 80 24 | } 25 | ], 26 | "no-unused-vars": "warn", 27 | "no-console": "off", 28 | "func-names": "off", 29 | "no-process-exit": "off", 30 | "object-shorthand": "off", 31 | "class-methods-use-this": "off" 32 | } 33 | } -------------------------------------------------------------------------------- /client/containers/Main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Form from '../components/Form' 3 | import SearchResults from '../components/SearchResults' 4 | import PreviousDonations from '../components/PreviousDonations' 5 | import { connect } from 'react-redux'; 6 | 7 | const mapStateToProps = (state) => ({ 8 | user: state.state.user 9 | }) 10 | 11 | 12 | class Main extends React.Component { 13 | constructor(props) { 14 | super(props); 15 | } 16 | 17 | render() { 18 | return ( 19 |
20 |
21 | 22 | 23 |
24 | ) 25 | } 26 | } 27 | 28 | export default connect(mapStateToProps, null)(Main); -------------------------------------------------------------------------------- /client/components/Favorites.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import store from '../store.js'; 4 | 5 | const mapStateToProps = state => ({ 6 | favorites: state.state.user 7 | }) 8 | 9 | class PreviousDonations extends React.Component { 10 | constructor(props) { 11 | super(props); 12 | } 13 | 14 | render() { 15 | console.log('in favs!') 16 | console.log('favorites:', this.props.favorites) 17 | 18 | return ( 19 |
20 | FAVORITES 21 | 22 | ) 23 | // const favs = []; 24 | // for (let i = 0; i < this.props.users.) 25 | } 26 | } 27 | 28 | export default connect(mapStateToProps, null)(PreviousDonations); 29 | -------------------------------------------------------------------------------- /client/components/Navbar.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { Link } from "react-router-dom"; 3 | import Form from './Form.jsx' 4 | 5 | const Navbar = (props) => { 6 | 7 | const [popup, setPopup] = useState(false); 8 | 9 | return ( 10 | 22 | ) 23 | } 24 | 25 | export default Navbar; -------------------------------------------------------------------------------- /client/components/PreviousDonations.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import PrevDonCard from './PrevDonCard'; 4 | 5 | const mapStateToProps = state => ({ 6 | user: state.state.user 7 | }) 8 | 9 | class PreviousDonations extends React.Component { 10 | constructor(props) { 11 | super(props); 12 | } 13 | 14 | render() { 15 | const charities = [] 16 | for (let i = 0; i < this.props.user.charities.length; i++) { 17 | charities.push( 18 | 19 | ) 20 | } 21 | return ( 22 |
23 | PREVIOUS DONATIONS 24 | {charities} 25 | 26 | ) 27 | } 28 | } 29 | 30 | export default connect(mapStateToProps, null)(PreviousDonations); 31 | -------------------------------------------------------------------------------- /__test__/client/reducer.test.js: -------------------------------------------------------------------------------- 1 | import reducer from '../../client/reducers/reducer'; 2 | 3 | describe('Should update state in consistent ways', () => { 4 | const initialState = { charities: [], user: {} }; 5 | it('Should set to initial state by default', () => { 6 | const result = reducer(); 7 | expect(result).toEqual(initialState); 8 | }) 9 | 10 | it('Should update charities if action.type === GET_CHARITIES', () => { 11 | const action = { type: 'GET_CHARITIES', payload: ['hello world'] } 12 | const result = reducer(initialState, action); 13 | expect(result.charities).toEqual(action.payload); 14 | }) 15 | 16 | it('Should update user if action.type === SAVE_USER', () => { 17 | const action = { type: 'SAVE_USER', payload: 'test' } 18 | const result = reducer(initialState, action); 19 | expect(result.user).toEqual(action.payload); 20 | }) 21 | }) -------------------------------------------------------------------------------- /server/models/userModel.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const { Schema } = mongoose; 4 | 5 | const url = 6 | process.env.NODE_ENV === 'development' 7 | ? process.env.DBURL 8 | : process.env.TESTDBURL; 9 | mongoose.connect(url).catch((err) => err); 10 | const db = mongoose.connection; 11 | db.on('error', console.error.bind(console, 'connection error: ')); 12 | db.once('open', () => { 13 | console.log('Connected to database successfully'); 14 | }); 15 | 16 | const userSchema = new Schema({ 17 | email: { type: String, unique: true }, 18 | username: { type: String, unique: true }, 19 | password: String, 20 | charities: [ 21 | { 22 | charityName: String, 23 | donationAmount: Number, 24 | lastDonation: Date, 25 | catImage: String, 26 | favorite: Boolean, 27 | }, 28 | ], 29 | }); 30 | 31 | module.exports = mongoose.model('Users', userSchema); 32 | -------------------------------------------------------------------------------- /server/routes/user.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const userController = require('../controllers/userController'); 3 | 4 | const router = express.Router(); 5 | 6 | router.post('/signUp', userController.addUser, (req, res) => { 7 | res.status(200).send(res.locals.user); 8 | }); 9 | 10 | router.post( 11 | '/makeAD', 12 | userController.getUserCharities, 13 | userController.parseUserCharities, 14 | userController.updateDatabaseUserCharities, 15 | (req, res) => res.status(200).send(res.locals.user) 16 | ); 17 | 18 | router.put( 19 | '/changeFav', 20 | userController.getUserCharities, 21 | userController.updateFav, 22 | userController.updateDatabaseUserCharities, 23 | (req, res) => res.status(200).send(res.locals.user) 24 | ); 25 | 26 | router.put('/login', userController.verifyUser, (req, res) => 27 | res.status(200).send(res.locals.user) 28 | ); 29 | 30 | module.exports = router; 31 | -------------------------------------------------------------------------------- /client/containers/Login.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import '../styles.css' 3 | import { Link } from "react-router-dom"; 4 | import LoginBox from '../components/LoginBox' 5 | import SignUpBox from '../components/SignUpBox' 6 | 7 | class Login extends React.Component { 8 | constructor() { 9 | super(); 10 | this.state = { 11 | displayLoginBox: true, 12 | } 13 | this.toggleLoginBox = this.toggleLoginBox.bind(this); 14 | } 15 | 16 | toggleLoginBox() { 17 | if (!this.state.displayLoginBox) this.setState({ displayLoginBox: true }) 18 | else this.setState({ displayLoginBox: false }); 19 | } 20 | 21 | 22 | render() { 23 | return ( 24 |
25 | {this.state.displayLoginBox ? 26 |
27 | 28 | 29 |
30 | : 31 |
32 | 33 | 34 |
35 | } 36 |
37 | ) 38 | } 39 | } 40 | 41 | export default Login; -------------------------------------------------------------------------------- /client/app.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | import { BrowserRouter as Router, Route, Routes} from 'react-router-dom'; 5 | 6 | 7 | import store from './store' 8 | import SearchResults from './components/SearchResults' 9 | import Login from './containers/Login'; 10 | import Main from './containers/Main'; 11 | 12 | //user/signup -> for account sign up 13 | //user/login -> for account login 14 | //main -> after logging in, main page 15 | //main/findCharities - > form for finding charities 16 | //user/makeAD -> make a donation 17 | 18 | class App extends React.Component { 19 | 20 | render() { 21 | return ( 22 |
23 | 24 | 25 | } /> 26 | } /> 27 | 28 | 29 |
30 | ) 31 | } 32 | } 33 | 34 | ReactDOM.render( 35 | ( 36 | 37 | 38 | 39 | ), 40 | document.getElementById('root') 41 | ); 42 | 43 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | require('dotenv').config(); 3 | 4 | const app = express(); 5 | app.use(express.json()); 6 | const PORT = 3000; 7 | const userRouter = require('./routes/user'); 8 | const mainRouter = require('./routes/main'); 9 | 10 | app.use(express.urlencoded({ extended: false })); 11 | 12 | // // Routes 13 | app.use('/main', mainRouter); 14 | app.use('/user', userRouter); 15 | // app.get('/test', (req, res) => res.status(200).send({ hello: 'world' })); // For testing purposes 16 | // Routes 17 | 18 | // Page not found 19 | app.use((req, res) => { 20 | res.status(404).send("This is not the page you're looking for..."); 21 | }); 22 | // Page not found 23 | 24 | // Error handling 25 | app.use((err, req, res) => { 26 | console.log('err', err) 27 | const defaultErr = { 28 | log: 'Express error handler caught unknown middleware error', 29 | status: 500, 30 | message: { err: 'An error occurred' }, 31 | }; 32 | const errorObj = Object.assign(defaultErr, err); 33 | console.log(errorObj.status, errorObj.log); 34 | return res.status(errorObj.status).json(errorObj.message); 35 | }); 36 | // Error handling 37 | 38 | // Server actually starts in ./server/start.js for testing purposes 39 | 40 | module.exports = app; 41 | -------------------------------------------------------------------------------- /client/components/SearchResults.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import store from '../store.js'; 3 | import { getCharitiesServ } from '../reducers/reducer.js'; 4 | import { connect } from 'react-redux'; 5 | import Card from './Card.jsx'; 6 | import { Link } from "react-router-dom"; 7 | 8 | const mapStateToProps = state => ({ 9 | charities: state.state.charities, 10 | username: state.state.user.username 11 | }) 12 | 13 | class SearchResults extends React.Component { 14 | constructor(props) { 15 | super(props); 16 | } 17 | 18 | render() { 19 | const charities = []; 20 | for (let i = 0; i < this.props?.charities.length; i++) { 21 | charities.push( 22 | <> 23 | 35 |
36 | ) 37 | } 38 | 39 | return ( 40 |
41 | {charities} 42 |
43 | ); 44 | } 45 | } 46 | 47 | export default connect(mapStateToProps, null)(SearchResults); -------------------------------------------------------------------------------- /client/components/LoginBox.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import axios from 'axios'; 3 | import { connect } from 'react-redux'; 4 | import { useNavigate, useNavigation } from 'react-router-dom'; 5 | import * as actions from '../actions/actions'; 6 | import styles from '../styles.css'; 7 | 8 | const mapDispatchToProps = (dispatch) => ({ 9 | saveUser: (userInfo) => dispatch(actions.SAVE_USER(userInfo)) 10 | }) 11 | 12 | const LoginBox = (props) => { 13 | const navigate = useNavigate() 14 | const submitLogin = (event, usernameText, passwordText) => { 15 | event.preventDefault(); 16 | const info = { 17 | username: usernameText.current.value, 18 | password: passwordText.current.value 19 | }; 20 | axios({ 21 | method: 'PUT', 22 | url: '/user/login', 23 | // mode: 'no-cors', 24 | headers: { 'Content-Type': 'application/json' }, 25 | data: info 26 | }) 27 | .then(data => { 28 | props.saveUser(data.data); 29 | navigate('/main'); 30 | }) 31 | .catch(err => alert('Sorry your login failed please try again')) 32 | } 33 | const usernameText = React.createRef(); 34 | const passwordText = React.createRef(); 35 | return ( 36 |
37 | submitLogin(event, usernameText, passwordText)}> 38 |
Aidwell
39 |
40 | 41 |
42 |
43 | 44 |
45 | 46 |
47 | ) 48 | 49 | } 50 | 51 | export default connect(null, mapDispatchToProps)(LoginBox); -------------------------------------------------------------------------------- /client/components/Card.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import axios from 'axios'; 4 | import * as actions from '../actions/actions'; 5 | 6 | const mapDispatchToProps = (dispatch) => ({ 7 | updateUser: (userInfo) => dispatch(actions.SAVE_USER(userInfo)) 8 | }) 9 | 10 | const Card = (props) => { 11 | const handleSubmit = () => { 12 | const donationAmount = window.prompt('Enter donation amount.'); 13 | const body = { 14 | charityName: props.name, 15 | donationAmount: donationAmount, 16 | username: props.username, 17 | catImage: props.catImage, 18 | causeImage: props.causeImage 19 | } 20 | 21 | axios({ 22 | method: 'POST', 23 | url: '/user/makeAD', 24 | // mode: 'no-cors', 25 | headers: { 'Content-Type': 'application/json' }, 26 | data: body 27 | }) 28 | .then(data => { 29 | console.log(data.data); 30 | props.updateUser(data.data); 31 | }) 32 | .catch(err => alert('Sorry donation failed. Please try again later')) 33 | } 34 | 35 | return ( 36 |
37 | 40 |

41 | {props.name}
42 | Overall Score: {props.overAllScore}
43 | Financial Score: {props.financialRating}
44 | Accountability Score: {props.accountabilityRating}
45 | Deductability Status: {props.deductibility}
46 | EIN: {props.ein}
47 |

48 |
49 | {props.mission} 50 |
51 |
52 | ) 53 | } 54 | 55 | export default connect(null, mapDispatchToProps)(Card); -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const path = require('path'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | 5 | module.exports = { 6 | entry: [ 7 | // entry point of our app 8 | './client/app.jsx', 9 | ], 10 | output: { 11 | path: path.resolve(__dirname, 'dist'), 12 | publicPath: '/', 13 | filename: 'bundle.js', 14 | }, 15 | devtool: 'eval-source-map', 16 | mode: 'development', 17 | devServer: { 18 | static: { 19 | // match the output path 20 | directory: path.resolve(__dirname, 'dist'), 21 | // match the output 'publicPath' 22 | publicPath: '/', 23 | }, 24 | /** 25 | * proxy is required in order to make api calls to 26 | * express server while using hot-reload webpack server 27 | * routes api fetch requests from localhost:8080/api/* (webpack dev server) 28 | * to localhost:3000/api/* (where our Express server is running) 29 | */ 30 | proxy: { 31 | '/user/**': 'http://localhost:3000/', 32 | 33 | '/main/**': 'http://localhost:3000/', 34 | 35 | }, 36 | }, 37 | module: { 38 | rules: [ 39 | { 40 | test: /.(js|jsx)$/, 41 | exclude: /node_modules/, 42 | use: { 43 | loader: 'babel-loader', 44 | options: { 45 | presets: ['@babel/preset-react', '@babel/preset-env'], 46 | plugins: [ 47 | '@babel/plugin-transform-runtime', 48 | '@babel/transform-async-to-generator', 49 | ], 50 | }, 51 | }, 52 | }, 53 | { 54 | test: /.(css|scss)$/, 55 | exclude: /node_modules/, 56 | use: ['style-loader', 'css-loader'], 57 | }, 58 | ], 59 | }, 60 | plugins: [ 61 | new HtmlWebpackPlugin({ 62 | template: './client/index.html', 63 | }), 64 | ], 65 | resolve: { 66 | // Enable importing JS / JSX files without specifying their extension 67 | extensions: ['.js', '.jsx'], 68 | }, 69 | }; 70 | -------------------------------------------------------------------------------- /client/components/SignUpBox.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import axios from 'axios'; 3 | import { connect } from 'react-redux'; 4 | import { useNavigate, useNavigation } from 'react-router-dom'; 5 | import * as actions from '../actions/actions'; 6 | 7 | const mapDispatchToProps = (dispatch) => ({ 8 | saveUser: (userInfo) => dispatch(actions.SAVE_USER(userInfo)) 9 | }) 10 | 11 | const LoginBox = (props) => { 12 | const navigate = useNavigate() 13 | 14 | const submitRegister = (event, usernameText, passwordText, emailText) => { 15 | event.preventDefault(); 16 | const info = { 17 | username: usernameText.current.value, 18 | email: emailText.current.value, 19 | password: passwordText.current.value 20 | }; 21 | axios({ 22 | method: 'POST', 23 | url: '/user/signUp', 24 | // mode: 'no-cors', 25 | headers: { 'Content-Type': 'application/json' }, 26 | data: info 27 | }) 28 | .then(data => { 29 | props.saveUser(data.data); 30 | navigate('/main') 31 | }) 32 | .catch(err => console.log('err:', err)) 33 | } 34 | 35 | 36 | const usernameText = React.createRef(); 37 | const passwordText = React.createRef(); 38 | const emailText = React.createRef(); 39 | return ( 40 |
41 |
submitRegister(event, usernameText, passwordText, emailText)}> 42 |
Register for an Account
43 |
44 | 45 |
46 | 47 |
48 |
49 | 50 |
51 |
52 |
53 | ) 54 | } 55 | 56 | export default connect(null, mapDispatchToProps)(LoginBox); -------------------------------------------------------------------------------- /client/components/PrevDonCard.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import axios from 'axios'; 4 | import * as actions from '../actions/actions'; 5 | 6 | const mapDispatchToProps = (dispatch) => ({ 7 | updateUser: (userInfo) => dispatch(actions.SAVE_USER(userInfo)) 8 | }) 9 | 10 | const PrevDonCard = (props) => { 11 | const handleSubmit = () => { 12 | const donationAmount = window.prompt('Enter donation amount.'); 13 | const body = { 14 | charityName: props.charityName, 15 | donationAmount: donationAmount, 16 | username: props.username 17 | } 18 | axios({ 19 | method: 'POST', 20 | url: '/user/makeAD', 21 | // mode: 'no-cors', 22 | headers: { 'Content-Type': 'application/json' }, 23 | data: body 24 | }) 25 | .then(data => { 26 | props.updateUser(data.data); 27 | }) 28 | .catch(err => alert('Sorry donation failed. Please try again later')) 29 | } 30 | 31 | const handleFavorite = () => { 32 | const body = { 33 | charityName: props.charityName, 34 | username: props.username, 35 | } 36 | axios({ 37 | method: 'PUT', 38 | url: '/user/changeFav', 39 | // mode: 'no-cors', 40 | headers: { 'Content-Type': 'application/json' }, 41 | data: body 42 | }) 43 | .then(data => { 44 | props.updateUser(data.data); 45 | }) 46 | .catch(err => alert('Sorry Fav Failed')) 47 | } 48 | 49 | return ( 50 |
51 | 54 |

55 | Charity Name: {props.charityName}
56 | Donation Amount: ${props.donationAmount}.00
57 | Last Donated: {new Date(props.lastDonation).toDateString()}
58 |

59 | 60 |
61 | ) 62 | } 63 | 64 | export default connect(null, mapDispatchToProps)(PrevDonCard); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Aidwell", 3 | "version": "1.0.0", 4 | "engines": { 5 | "node": ">=8.3.0" 6 | }, 7 | "description": "", 8 | "main": "server/server.js", 9 | "scripts": { 10 | "server": "nodemon ./server/start.js", 11 | "dev": "concurrently \"cross-env NODE_ENV=development webpack-dev-server --open --hot --progress --color \" \"nodemon ./server/start.js\"", 12 | "test": "jest --setupFiles dotenv/config --forceExit" 13 | }, 14 | "dependencies": { 15 | "@material-ui/icons": "^4.11.2", 16 | "axios": "^0.26.1", 17 | "dotenv": "^16.0.0", 18 | "express": "^4.16.3", 19 | "mongoose": "^6.1.8", 20 | "pg": "^8.5.1", 21 | "react": "^16.5.2", 22 | "react-dom": "^16.5.2", 23 | "react-hot-loader": "^4.6.3", 24 | "react-redux": "^7.2.6", 25 | "react-router": "^4.3.1", 26 | "react-router-dom": "^6.2.2-pre.0", 27 | "redux": "^4.1.2", 28 | "redux-thunk": "^2.4.1" 29 | }, 30 | "devDependencies": { 31 | "@babel/core": "^7.1.2", 32 | "@babel/plugin-transform-runtime": "^7.17.0", 33 | "@babel/preset-env": "^7.1.0", 34 | "@babel/preset-react": "^7.0.0", 35 | "@testing-library/react": "^12.1.4", 36 | "babel-eslint": "^10.1.0", 37 | "babel-loader": "^8.2.3", 38 | "concurrently": "^5.0.0", 39 | "cross-env": "^6.0.3", 40 | "css-loader": "^6.7.1", 41 | "eslint": "^7.32.0", 42 | "eslint-config-airbnb": "^19.0.4", 43 | "eslint-config-node": "^4.1.0", 44 | "eslint-config-prettier": "^8.5.0", 45 | "eslint-plugin-import": "^2.25.4", 46 | "eslint-plugin-jsx-a11y": "^6.5.1", 47 | "eslint-plugin-node": "^11.1.0", 48 | "eslint-plugin-prettier": "^4.0.0", 49 | "eslint-plugin-react": "^7.29.4", 50 | "eslint-plugin-react-hooks": "^4.3.0", 51 | "file-loader": "^6.2.0", 52 | "html-webpack-plugin": "^5.5.0", 53 | "jest": "^27.5.1", 54 | "nodemon": "^1.18.9", 55 | "prettier": "^2.5.1", 56 | "style-loader": "^3.3.1", 57 | "supertest": "^6.2.2", 58 | "webpack": "^5.64.1", 59 | "webpack-cli": "^4.9.1", 60 | "webpack-dev-server": "^4.5.0", 61 | "webpack-hot-middleware": "^2.24.3" 62 | }, 63 | "nodemonConfig": { 64 | "ignore": [ 65 | "server/data/*", 66 | "client/*" 67 | ] 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /__test__/server/controllers/userController.test.js: -------------------------------------------------------------------------------- 1 | const { 2 | updateFav, 3 | parseUserCharities, 4 | verifyUser, 5 | addUser, 6 | getUserCharities, 7 | updateDatabaseUserCharities, 8 | } = require('../../../server/controllers/userController'); 9 | 10 | describe('userController testing suite', () => { 11 | let req; 12 | let res; 13 | let next; 14 | beforeEach(() => { 15 | req = { body: {} }; 16 | res = { locals: {} }; 17 | next = () => { }; 18 | }); 19 | 20 | it('Should have an updateFav function', () => { 21 | expect(typeof updateFav).toBe('function'); 22 | }); 23 | 24 | it('Should update favorite charity', () => { 25 | req.body.charityName = 'test charity'; 26 | res.locals.user = { 27 | username: 'test name', 28 | charities: [{ charityName: 'test charity', favorite: false }], 29 | }; 30 | const updatedUser = { 31 | username: 'test name', 32 | charities: [{ charityName: 'test charity', favorite: true }], 33 | }; 34 | updateFav(req, res, next); 35 | expect(res.locals.user).toEqual(updatedUser); 36 | }); 37 | 38 | it('Should have an parseUserCharities function', () => { 39 | expect(typeof parseUserCharities).toBe('function'); 40 | }); 41 | 42 | it('Should update an already existing charity', () => { 43 | req.body.charityName = 'test charity'; 44 | req.body.donationAmount = 10; 45 | req.body.username = 'test user'; 46 | req.body.catImage = ''; 47 | req.body.causeImage = ''; 48 | res.locals.user = { 49 | username: 'test name', 50 | charities: [{ charityName: 'test charity', donationAmount: 15 }], 51 | }; 52 | parseUserCharities(req, res, next); 53 | expect(res.locals.user.charities[0].donationAmount).toEqual(25); 54 | }); 55 | 56 | it('Should add a new charity', () => { 57 | req.body.charityName = 'test charity'; 58 | req.body.donationAmount = 10; 59 | req.body.username = 'test user'; 60 | req.body.catImage = ''; 61 | req.body.causeImage = ''; 62 | res.locals.user = { 63 | username: 'test name', 64 | charities: [], 65 | }; 66 | parseUserCharities(req, res, next); 67 | expect(res.locals.user.charities[0].donationAmount).toEqual(10); 68 | }); 69 | 70 | it('Should have an verifyUser function', () => { 71 | expect(typeof verifyUser).toBe('function'); 72 | }); 73 | 74 | it('Should have an addUser function', () => { 75 | expect(typeof addUser).toBe('function'); 76 | }); 77 | 78 | it('Should have an getUserCharities function', () => { 79 | expect(typeof getUserCharities).toBe('function'); 80 | }); 81 | 82 | it('Should have an updateDatabaseUserCharities function', () => { 83 | expect(typeof updateDatabaseUserCharities).toBe('function'); 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /server/controllers/mainController.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign */ 2 | const axios = require('axios'); 3 | 4 | const mainController = { 5 | buildQuery(req, res, next) { 6 | res.locals.query = process.env.APIURL; 7 | if ('search' in req.body && typeof req.body.search === 'string') 8 | res.locals.query += `&search=${req.body.search}`; 9 | if ( 10 | 'fundraisingOrgs' in req.body && 11 | typeof req.body.fundraisingOrgs === 'boolean' 12 | ) 13 | res.locals.query += `&fundraisingOrgs=${req.body.fundraisingOrgs}`; 14 | if ('state' in req.body && req.body.state.length === 2) 15 | res.locals.query += `&state=${req.body.state.toUpperCase()}`; 16 | if ('city' in req.body && typeof req.body.city === 'string') 17 | res.locals.query += `&city=${req.body.city}`; 18 | if ('zip' in req.body && req.body.zip.length === 5) 19 | res.locals.query += `&zip=${req.body.zip}`; 20 | if ('sizeRange' in req.body) 21 | if (req.body.sizeRange.toLowerCase() === 'small') 22 | res.locals.query += '&sizeRange=1'; 23 | else if (req.body.sizeRange.toLowerCase() === 'medium') 24 | res.locals.query += '&sizeRange=2'; 25 | else if (req.body.sizeRange.toLowerCase() === 'large') 26 | res.locals.query += '&sizeRange=3'; 27 | if ( 28 | 'donorPrivacy' in req.body && 29 | typeof req.body.donorPrivacy === 'boolean' 30 | ) 31 | res.locals.query += `&donorPrivacy=${req.body.donorPrivacy}`; 32 | const possibleScopes = { 33 | ALL: true, 34 | REGIONAL: true, 35 | NATIONAL: true, 36 | INTERNATIONAL: true, 37 | }; 38 | if ( 39 | 'scopeOfWork' in req.body && 40 | req.body.scopeOfWork.toUpperCase() in possibleScopes 41 | ) { 42 | res.locals.query += `&scopeOfWork=${req.body.scopeOfWork.toUpperCase()}`; 43 | } 44 | if ( 45 | 'noGovSupport' in req.body && 46 | typeof req.body.noGovSupport === 'boolean' 47 | ) 48 | res.locals.query += `&noGovSupport=${req.body.noGovSupport}`; 49 | 50 | return next(); 51 | }, 52 | 53 | getCharities(req, res, next) { 54 | axios.get(res.locals.query) 55 | .then((charities) => { 56 | res.locals.raw = charities.data; 57 | next(); 58 | }) 59 | .catch((err) => { 60 | err.status = err.response.status; 61 | err.log = 'Error in mainController getCharities middleware.'; 62 | next(err); 63 | }); 64 | }, 65 | 66 | processCharities(req, res, next) { 67 | if (typeof res.locals.raw !== 'object') 68 | return next({ 69 | log: 'Error in mainController processCharities middleware.', 70 | }); 71 | 72 | res.locals.parsed = res.locals.raw.reduce((acc, curr) => { 73 | const charity = {}; 74 | charity.name = curr.organization?.charityName; 75 | charity.mission = curr.mission; 76 | charity.link = curr.charityNavigatorURL; 77 | charity.catImage = curr.category?.image; 78 | charity.causeImage = curr.cause?.image; 79 | if (curr.currentRating) { 80 | charity.overAllScore = curr.currentRating.score; 81 | charity.financialRating = curr.currentRating.financialRating?.score; 82 | charity.accountabilityRating = 83 | curr.currentRating.accountabilityRating?.score; 84 | } 85 | charity.deductibility = curr.irsClassification?.deductibility; 86 | charity.ein = curr.organization?.ein; 87 | 88 | acc.push(charity); 89 | return acc; 90 | }, []); 91 | return next(); 92 | }, 93 | }; 94 | 95 | module.exports = mainController; 96 | -------------------------------------------------------------------------------- /client/styles.css: -------------------------------------------------------------------------------- 1 | 2 | @import url('https://fonts.googleapis.com/css2?family=Playfair+Display+SC:wght@700&display=swap'); 3 | body {background-color: rgb(173, 209, 240)} 4 | 5 | .loginBox { 6 | display: flex; 7 | flex-direction: row; 8 | justify-content: space-around; 9 | align-content: center; 10 | font-size: 60px; 11 | border: 1px solid rgb(0, 74, 124); 12 | min-width: 100px; 13 | min-height: 300px; 14 | border-radius: 10px; 15 | background-color: rgb(232, 241, 245); 16 | box-shadow: 5px 5px 3px grid-template-columns; 17 | } 18 | 19 | .frontpageText{ 20 | display: flex; 21 | flex-direction: column; 22 | /* justify-content: space-around; */ 23 | align-items: center; 24 | font-size: 60px; 25 | font-family: 'Playfair Display SC', serif; 26 | border: 5px solid rgb(232, 241, 245); 27 | min-width: 100px; 28 | min-height: 300px; 29 | border-radius: 10px; 30 | background-color:rgb(159, 212, 247); 31 | box-shadow: 5px 5px 3px grid-template-columns; 32 | } 33 | 34 | .favs{ 35 | font-family: 'Playfair Display SC', serif; 36 | } 37 | 38 | .loginText{ 39 | font-size: 80px; 40 | justify-content: center; 41 | font-family: 'Playfair Display SC', serif; 42 | display: flex; 43 | flex-direction: row; 44 | } 45 | 46 | .singupText{ 47 | font-size: 40px; 48 | justify-content: center; 49 | font-family: 'Playfair Display SC', serif; 50 | display: flex; 51 | flex-direction: row; 52 | } 53 | 54 | .signupBox { 55 | display: flex; 56 | flex-direction: column; 57 | justify-content: space-around; 58 | align-items: center; 59 | min-width: 100px; 60 | min-height: 300px; 61 | border: 1px solid rgb(0, 74, 124); 62 | background-color: rgb(232, 241, 245); 63 | font-size: 30px; 64 | box-shadow: 5px 5px 3px; 65 | } 66 | 67 | .buttonStyle{ 68 | min-width: 100%; 69 | border: 1px solid rgb(2, 53, 98); 70 | background-color: rgb(164, 190, 201); 71 | box-shadow: 3px 3px 2px rgb(0, 74, 124); 72 | padding: 13px; 73 | font-size: 20px; 74 | border-radius: 10px; 75 | font-family: 'Playfair Display SC', serif; 76 | font-weight: bold; 77 | color: rgb(0, 74, 124) 78 | } 79 | 80 | .gridContainer { 81 | display: inline-grid; 82 | grid-template-columns: 2fr 3fr 1fr; 83 | column-gap: 10px; 84 | } 85 | 86 | .charitycard { 87 | display: flex; 88 | background-color: rgb(232, 241, 245); 89 | grid-column-start: 2; 90 | grid-column-end: 2; 91 | flex-direction: row; 92 | align-self: stretch; 93 | justify-content: flex-end; 94 | /* width:200; */ 95 | /* inline-size: min-content; */ 96 | border: 2px solid; 97 | box-shadow: 0 1px 3px 1px; 98 | } 99 | 100 | .image { 101 | width: 100px; 102 | height: 100px; 103 | object-fit: cover; 104 | flex-shrink: 0; 105 | } 106 | 107 | .information { 108 | display: flex; 109 | flex-direction: row; 110 | justify-content: center; 111 | width: 100%; 112 | padding: 12px 20px 12px 22px; 113 | } 114 | 115 | .ratings { 116 | display: flex; 117 | flex-wrap: wrap; 118 | flex-direction: row; 119 | justify-content: center; 120 | width: 500px; 121 | padding: 12px 20px 12px 22px; 122 | } 123 | 124 | .previousDonations { 125 | grid-column-start: 2; 126 | grid-column-end: 2; 127 | } 128 | 129 | .formBox { 130 | border:2px solid; 131 | border-color: rgb(rgb(11, 60, 84)); 132 | border-radius: 5px; 133 | max-height:340px; 134 | font-family: 'Playfair Display SC', serif; 135 | } 136 | 137 | .prevDonCard { 138 | grid-column-start: 3; 139 | grid-column-end: 3; 140 | display: flex; 141 | flex-direction: row; 142 | align-self: stretch; 143 | width: 100%; 144 | max-width: 400px; 145 | max-height: 110px; 146 | border: 1px solid; 147 | box-shadow: 0 2px 5px 2px; 148 | } 149 | 150 | .prevDetails { 151 | display: flex; 152 | flex-wrap: wrap; 153 | flex-direction: row; 154 | justify-content: start; 155 | } 156 | 157 | .prevImage { 158 | width: 90px; 159 | height: 90px; 160 | object-fit: cover; 161 | flex-shrink: 0; 162 | } 163 | 164 | .favButton { 165 | float: right; 166 | border: 1px solid coral; 167 | margin: auto; 168 | } 169 | -------------------------------------------------------------------------------- /server/controllers/userController.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-restricted-syntax */ 2 | /* eslint-disable no-param-reassign */ 3 | /* eslint-disable consistent-return */ 4 | const db = require('../models/userModel'); 5 | 6 | const userController = { 7 | verifyUser(req, res, next) { 8 | const { username, password } = req.body; 9 | db.find({ username }, { username: 1, password: 1, charities: 1 }) 10 | .then((data) => { 11 | const checkPassword = password === data[0].password; 12 | if (!checkPassword) 13 | return next( 14 | new Error( 15 | 'Invalid credentials in userController verifyUser middleware.' 16 | ) 17 | ); 18 | res.locals.user = { 19 | username: data[0].username, 20 | charities: data[0].charities, 21 | }; 22 | return next(); 23 | }) 24 | .catch(() => 25 | next( 26 | new Error( 27 | 'Invalid credentials in userController verifyUser middleware.' 28 | ) 29 | ) 30 | ); 31 | }, 32 | 33 | addUser(req, res, next) { 34 | const { email, username, password } = req.body; 35 | const missingArg = !email || !username || !password; 36 | const badEmail = 37 | typeof email !== 'string' || !email.includes('@') || !email.includes('.'); 38 | const badUsername = typeof username !== 'string'; 39 | const badPassword = typeof password !== 'string'; 40 | if (missingArg || badEmail || badUsername || badPassword) 41 | return next( 42 | new Error('Invalid credentials in userController adUser middleware.') 43 | ); 44 | 45 | db.create( 46 | { 47 | email, 48 | username, 49 | password, 50 | charities: [], 51 | }, 52 | (err, newUser) => { 53 | if (err) return next(err); 54 | // eslint-disable-next-line no-shadow 55 | const { username, charities } = newUser; 56 | res.locals.user = { username, charities }; 57 | return next(); 58 | } 59 | ); 60 | }, 61 | 62 | getUserCharities(req, res, next) { 63 | const { charityName, username } = req.body; 64 | if (!charityName || !username) 65 | return next( 66 | new Error( 67 | 'Missing information in userController getUserCharities middleware' 68 | ) 69 | ); 70 | 71 | db.find({ username }, { username: 1, charities: 1 }) 72 | .then((data) => { 73 | // eslint-disable-next-line prefer-destructuring 74 | res.locals.user = data[0]; 75 | next(); 76 | }) 77 | .catch(() => 78 | next(new Error('Invalid username please logout and try again.')) 79 | ); 80 | }, 81 | 82 | parseUserCharities(req, res, next) { 83 | const { charityName, donationAmount, catImage, causeImage } = req.body; 84 | if (!donationAmount) 85 | return next( 86 | new Error('Missing donationAmount userController parseUserCharities') 87 | ); 88 | const { user } = res.locals; 89 | let found = false; 90 | for (const charity of user.charities) { 91 | if (charity.charityName === charityName) { 92 | found = true; 93 | charity.lastDonation = new Date(); 94 | charity.donationAmount += parseInt(donationAmount, 10); 95 | break; 96 | } 97 | } 98 | if (!found) 99 | user.charities.push({ 100 | charityName, 101 | donationAmount: parseInt(donationAmount, 10), 102 | catImage, 103 | causeImage, 104 | favorite: false, 105 | lastDonation: new Date(), 106 | }); 107 | res.locals.user = user; 108 | next(); 109 | }, 110 | 111 | updateDatabaseUserCharities(req, res, next) { 112 | const { user } = res.locals; 113 | res.locals.user = { username: user.username, charities: user.charities }; 114 | db.updateOne( 115 | { username: user.username }, 116 | { $set: { charities: user.charities } } 117 | ) 118 | .then((resp) => { 119 | if (resp.modifiedCount < 1) 120 | return next( 121 | new Error( 122 | 'Failed to update userController updateDatabaseUserCharities.' 123 | ) 124 | ); 125 | return next(); 126 | }) 127 | .catch(() => 128 | next( 129 | new Error( 130 | 'Invalid username at updateOne userController updateDatabaseUserCharities.' 131 | ) 132 | ) 133 | ); 134 | }, 135 | 136 | updateFav(req, res, next) { 137 | const { charityName } = req.body; 138 | const { user } = res.locals; 139 | for (const charity of user.charities) { 140 | if (charity.charityName === charityName) { 141 | charity.favorite = !charity.favorite; 142 | break; 143 | } 144 | } 145 | res.locals.user = user; 146 | next(); 147 | }, 148 | }; 149 | 150 | module.exports = userController; 151 | -------------------------------------------------------------------------------- /client/components/Form.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import axios from 'axios'; 3 | import * as actions from '../actions/actions' 4 | import { connect } from 'react-redux'; 5 | 6 | const mapDispatchToProps = (dispatch) => ({ 7 | addCharitiesToState: (charityInfo) => { 8 | dispatch(actions.GET_CHARITIES(charityInfo)) 9 | } 10 | }) 11 | 12 | const Form = (props) => { 13 | 14 | const createBody = () => { 15 | return { 16 | search: document.getElementById('search').value, 17 | state: document.getElementById('State').value, 18 | city: document.getElementById('City').value, 19 | zip: document.getElementById('Zipcode').value, 20 | sizeRange: document.getElementById('size').value === 'true' ? '' : document.getElementById('size').value, //? 21 | scopeOfWork: document.getElementById('scopeofwork').value === 'true' ? '' : document.getElementById('scopeofwork').value, //? 22 | fundraisingOrgs: document.getElementById('fundraisingOrgs').value === 'true' || document.getElementById('fundraisingOrgs').value === '' ? true : false, 23 | donorPrivacy: document.getElementById('donorPrivacy').value === 'true' || document.getElementById('donorPrivacy').value === '' ? true : false, 24 | // noGovSupport: document.getElementById('noGovSupport').value === 'true' || document.getElementById('noGovSupport').value === '' ? true : false, 25 | } 26 | } 27 | 28 | const resetFields = () => { 29 | document.getElementById('search').value = ''; 30 | document.getElementById('State').value = ''; 31 | document.getElementById('City').value = ''; 32 | document.getElementById('Zipcode').value = ''; 33 | } 34 | 35 | const makeServerCall = (reqBody) => { 36 | 37 | axios({ 38 | method: 'PUT', 39 | url: '/main/findCharities', 40 | // mode: 'no-cors', 41 | headers: { 'Content-Type': 'application/json' }, 42 | data: reqBody 43 | }) 44 | .then(data => props.addCharitiesToState(data.data)) 45 | .catch(err => { 46 | window.alert('No results available. Please try again.'); 47 | console.log('err:', err) 48 | } 49 | ) 50 | } 51 | 52 | const handleSubmit = (el) => { 53 | el.preventDefault(); 54 | const body = createBody(); 55 | console.log(body) 56 | // resetFields(); 57 | makeServerCall(body); 58 | } 59 | 60 | return ( 61 |
Charity Search
62 |
63 | 64 |
65 | 66 | 67 |
68 | 69 | 70 |
71 | 72 | 73 |
74 | 75 |
76 | 77 | 81 |
82 | 83 |
84 | 85 | 90 |
91 | 92 | 93 |
94 | 95 | 99 |
100 | 101 |
102 | 103 | 109 |
110 | 111 | {/*
112 | 113 | 117 |
*/} 118 | 119 |
120 |
121 | ) 122 | }; 123 | 124 | export default connect(null, mapDispatchToProps)(Form); -------------------------------------------------------------------------------- /__test__/server/endpoints.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable node/no-unpublished-require */ 2 | import 'regenerator-runtime/runtime'; 3 | const supertest = require('supertest'); 4 | const mongoose = require('mongoose'); 5 | const app = require('../../server/server'); 6 | 7 | const request = supertest(app); 8 | 9 | describe('/', () => { 10 | beforeAll((done) => { 11 | if (!mongoose.connection.db) { 12 | mongoose.connection.on('connected', done); 13 | } else { 14 | done(); 15 | } 16 | }, 20000); 17 | 18 | afterAll((done) => { 19 | mongoose.disconnect(); 20 | done(); 21 | }); 22 | 23 | const genRanInfo = (reqChar = '') => 24 | String(Math.floor(Math.random() * 1000000000)) + reqChar; 25 | 26 | it('Gets 404 status on bad request', async () => { 27 | const resp = await request.get('/;lkajsdfasl;kj'); 28 | expect(resp.status).toBe(404); 29 | }); 30 | 31 | it('Errors with no body information /user/changeFav', async () => { 32 | const resp = await request.put('/user/changeFav'); 33 | expect(resp.status).toBe(500); 34 | }); 35 | 36 | it('Successfully toggles favorite with /user/changeFav', async () => { 37 | let resp = await request 38 | .put('/user/changeFav') 39 | .send({ username: 'me', charityName: 'Test 5' }); 40 | const newValue = resp.body.charities[0].favorite; 41 | expect(resp.status).toBe(200); 42 | expect(typeof newValue).toBe('boolean'); 43 | resp = await request 44 | .put('/user/changeFav') 45 | .send({ username: 'me', charityName: 'Test 5' }); 46 | expect(resp.status).toBe(200); 47 | const originalValue = resp.body.charities[0].favorite; 48 | expect(typeof originalValue).toBe('boolean'); 49 | expect(!originalValue).toBe(newValue); 50 | }); 51 | 52 | it('Errors with no body information /user/makeAD', async () => { 53 | const resp = await request.post('/user/makeAD'); 54 | expect(resp.status).toBe(500); 55 | }); 56 | 57 | it('Successfully adds donation with /user/makeAD', async () => { 58 | let resp = await request 59 | .post('/user/makeAD') 60 | .send({ username: 'me', charityName: 'Test 5', donationAmount: 25 }); 61 | const newValue = resp.body.charities[0].donationAmount; 62 | expect(resp.status).toBe(200); 63 | expect(typeof newValue).toBe('number'); 64 | resp = await request 65 | .post('/user/makeAD') 66 | .send({ username: 'me', charityName: 'Test 5', donationAmount: 25 }); 67 | expect(resp.status).toBe(200); 68 | const updatedValue = resp.body.charities[0].donationAmount; 69 | expect(typeof updatedValue).toBe('number'); 70 | expect(updatedValue).toBe(newValue + 25); 71 | }); 72 | 73 | it('Errors with no body information /user/makeAD', async () => { 74 | const resp = await request.post('/user/makeAD'); 75 | expect(resp.status).toBe(500); 76 | }); 77 | 78 | it('Successfully adds donation with /user/makeAD', async () => { 79 | let resp = await request 80 | .post('/user/makeAD') 81 | .send({ username: 'me', charityName: 'Test 5', donationAmount: 25 }); 82 | const newValue = resp.body.charities[0].donationAmount; 83 | expect(resp.status).toBe(200); 84 | expect(typeof newValue).toBe('number'); 85 | resp = await request 86 | .post('/user/makeAD') 87 | .send({ username: 'me', charityName: 'Test 5', donationAmount: 25 }); 88 | expect(resp.status).toBe(200); 89 | const updatedValue = resp.body.charities[0].donationAmount; 90 | expect(typeof updatedValue).toBe('number'); 91 | expect(updatedValue).toBe(newValue + 25); 92 | }); 93 | 94 | it('Successfully adds user if username, email, and password are input /user/signUp', async () => { 95 | const newUser = {}; 96 | let resp = await request.post('/user/signUp').send(newUser); 97 | expect(resp.status).toBe(500); 98 | newUser.username = genRanInfo(); 99 | resp = await request.post('/user/signUp').send(newUser); 100 | expect(resp.status).toBe(500); 101 | newUser.email = genRanInfo('@.'); 102 | resp = await request.post('/user/signUp').send(newUser); 103 | expect(resp.status).toBe(500); 104 | newUser.password = genRanInfo(); 105 | resp = await request.post('/user/signUp').send(newUser); 106 | expect(resp.status).toBe(200); 107 | expect(resp.body.charities).toEqual([]); 108 | }); 109 | 110 | it('Successfully adds donation with /user/makeAD', async () => { 111 | let resp = await request 112 | .post('/user/makeAD') 113 | .send({ username: 'me', charityName: 'Test 5', donationAmount: 25 }); 114 | const newValue = resp.body.charities[0].donationAmount; 115 | expect(resp.status).toBe(200); 116 | expect(typeof newValue).toBe('number'); 117 | resp = await request 118 | .post('/user/makeAD') 119 | .send({ username: 'me', charityName: 'Test 5', donationAmount: 25 }); 120 | expect(resp.status).toBe(200); 121 | const updatedValue = resp.body.charities[0].donationAmount; 122 | expect(typeof updatedValue).toBe('number'); 123 | expect(updatedValue).toBe(newValue + 25); 124 | }); 125 | }); 126 | -------------------------------------------------------------------------------- /__test__/server/controllers/mainController.test.js: -------------------------------------------------------------------------------- 1 | const { 2 | buildQuery, 3 | processCharities, 4 | getCharities, 5 | } = require('../../../server/controllers/mainController'); 6 | 7 | describe('mainController testing suite', () => { 8 | let req; 9 | let res; 10 | let next; 11 | beforeEach(() => { 12 | req = { body: {} }; 13 | res = { locals: {} }; 14 | next = () => { }; 15 | }); 16 | 17 | it('Should have a buildQuery method', () => { 18 | expect(typeof buildQuery).toBe('function'); 19 | }); 20 | 21 | it('buidQuery should ignore invalid inputs', () => { 22 | req.body.state = 1; 23 | req.body.fundraisingOrgs = 2; 24 | req.body.city = 12312; 25 | req.body.zip = 123; 26 | req.body.sizeRange = 'terrible'; 27 | req.body.donorPrivacy = 23; 28 | req.body.scopeOfWork = 'menial'; 29 | req.body.noGovSupport = 'blue'; 30 | buildQuery(req, res, next); 31 | expect('query' in res.locals).toBe(true); 32 | expect(res.locals.query.includes('Size=20')).toBe(true); 33 | }); 34 | 35 | it('buidQuery should accept valid inputs', () => { 36 | req.body.state = 'tx'; 37 | req.body.fundraisingOrgs = true; 38 | req.body.city = 'LA'; 39 | req.body.zip = '12345'; 40 | req.body.sizeRange = 'small'; 41 | req.body.donorPrivacy = true; 42 | req.body.scopeOfWork = 'international'; 43 | req.body.noGovSupport = true; 44 | buildQuery(req, res, next); 45 | const { query } = res.locals; 46 | expect(query.includes('state')).toBe(true); 47 | expect(query.includes('fundraisingOrgs')).toBe(true); 48 | expect(query.includes('city')).toBe(true); 49 | expect(query.includes('zip')).toBe(true); 50 | expect(query.includes('sizeRange')).toBe(true); 51 | expect(query.includes('donorPrivacy')).toBe(true); 52 | expect(query.includes('scopeOfWork')).toBe(true); 53 | expect(query.includes('noGovSupport')).toBe(true); 54 | }); 55 | 56 | it('Should have a getCharities method', () => { 57 | expect(typeof getCharities).toBe('function'); 58 | }); 59 | 60 | it('Should have a processCharities method', () => { 61 | expect(typeof processCharities).toBe('function'); 62 | }); 63 | 64 | it('Should process charities data', () => { 65 | res.locals.raw = [ 66 | { 67 | charityNavigatorURL: 68 | 'https://www.charitynavigator.org/?bay=search.summary&orgid=7516&utm_source=DataAPI&utm_content=9989397e', 69 | mission: 70 | "Celebrating Maine's role in American art, the Farnsworth Art Museum offers a nationally recognized collection of works from many of America's greatest artists. With 20,000 square feet of gallery space and over 10,000 works in the collection, there is always something new on view at the Farnsworth. The museum houses the nation's second-largest collection of works by premier 20th-century sculptor Louise Nevelson. Its Wyeth Center exclusively features works of Andrew, N.C. and Jamie Wyeth. The Farnsworth's library is also housed in its Rockland, ME, campus. Two historic buildings, the Farnsworth Homestead and the Olson House, complete the museum complex.", 71 | websiteURL: 'http://www.farnsworthmuseum.org/', 72 | tagLine: "Celebrating Maine's role in American art", 73 | charityName: 'Farnsworth Art Museum', 74 | ein: '010368070', 75 | currentRating: { 76 | score: 86.04, 77 | ratingImage: [Object], 78 | rating: 3, 79 | _rapid_links: [Object], 80 | financialRating: [Object], 81 | accountabilityRating: [Object], 82 | }, 83 | category: { 84 | categoryName: 'Arts, Culture, Humanities', 85 | categoryID: 2, 86 | charityNavigatorURL: 87 | 'https://www.charitynavigator.org/index.cfm?bay=search.categories&categoryid=2&utm_source=DataAPI&utm_content=9989397e', 88 | image: 89 | 'https://d20umu42aunjpx.cloudfront.net/_gfx_/icons/categories/arts.png?utm_source=DataAPI&utm_content=9989397e', 90 | }, 91 | cause: { 92 | causeID: 3, 93 | causeName: 'Museums', 94 | charityNavigatorURL: 95 | 'https://www.charitynavigator.org/index.cfm?bay=search.results&cgid=2&cuid=3&utm_source=DataAPI&utm_content=9989397e', 96 | image: 97 | 'https://d20umu42aunjpx.cloudfront.net/_gfx_/causes/small/museums.gif?utm_source=DataAPI&utm_content=9989397e', 98 | }, 99 | irsClassification: { 100 | deductibility: 'Contributions are deductible', 101 | subsection: '501(c)(3)', 102 | assetAmount: 32899602, 103 | nteeType: 'Arts, Culture and Humanities', 104 | incomeAmount: 6770476, 105 | nteeSuffix: '0', 106 | filingRequirement: '990 (all other) or 990EZ return', 107 | classification: 'Educational Organization', 108 | latest990: 'September, 2020', 109 | rulingDate: 'April, 1987', 110 | nteeCode: 'A51', 111 | groupName: null, 112 | deductibilityCode: '1', 113 | affiliation: 114 | 'Independent - the organization is an independent organization or an independent auxiliary (i.e., not affiliated with a National, Regional, or Geographic grouping of organizations).', 115 | foundationStatus: 116 | 'Organization which receives a substantial part of its support from a governmental unit or the general public 170(b)(1)(A)(vi)', 117 | nteeClassification: 'Art Museums', 118 | accountingPeriod: 'September', 119 | deductibilityDetail: null, 120 | exemptOrgStatus: 'Unconditional Exemption', 121 | exemptOrgStatusCode: '01', 122 | nteeLetter: 'A', 123 | }, 124 | mailingAddress: { 125 | country: null, 126 | stateOrProvince: 'ME', 127 | city: 'Rockland', 128 | postalCode: '04841', 129 | streetAddress1: '16 Museum Street', 130 | streetAddress2: null, 131 | }, 132 | advisories: { severity: null, active: [Object] }, 133 | organization: { 134 | charityName: 'Farnsworth Art Museum', 135 | ein: '010368070', 136 | charityNavigatorURL: 137 | 'https://www.charitynavigator.org/?bay=search.summary&orgid=7516&utm_source=DataAPI&utm_content=9989397e', 138 | _rapid_links: [Object], 139 | }, 140 | }, 141 | { 142 | charityNavigatorURL: 143 | 'https://www.charitynavigator.org/?bay=search.summary&orgid=4856&utm_source=DataAPI&utm_content=9989397e', 144 | mission: 145 | 'The Portland Museum of Art strives to engage audiences in a dialogue about the relevance of art and culture to our everyday lives and it committed to the stewardship and growth of the collection.', 146 | websiteURL: 'http://www.portlandmuseum.org', 147 | tagLine: ' ', 148 | charityName: 'Portland Museum of Art', 149 | ein: '010378420', 150 | currentRating: { 151 | score: 90.67, 152 | ratingImage: [Object], 153 | rating: 4, 154 | _rapid_links: [Object], 155 | financialRating: [Object], 156 | accountabilityRating: [Object], 157 | }, 158 | category: { 159 | categoryName: 'Arts, Culture, Humanities', 160 | categoryID: 2, 161 | charityNavigatorURL: 162 | 'https://www.charitynavigator.org/index.cfm?bay=search.categories&categoryid=2&utm_source=DataAPI&utm_content=9989397e', 163 | image: 164 | 'https://d20umu42aunjpx.cloudfront.net/_gfx_/icons/categories/arts.png?utm_source=DataAPI&utm_content=9989397e', 165 | }, 166 | cause: { 167 | causeID: 3, 168 | causeName: 'Museums', 169 | charityNavigatorURL: 170 | 'https://www.charitynavigator.org/index.cfm?bay=search.results&cgid=2&cuid=3&utm_source=DataAPI&utm_content=9989397e', 171 | image: 172 | 'https://d20umu42aunjpx.cloudfront.net/_gfx_/causes/small/museums.gif?utm_source=DataAPI&utm_content=9989397e', 173 | }, 174 | irsClassification: { 175 | deductibility: 'Contributions are deductible', 176 | subsection: '501(c)(3)', 177 | assetAmount: 75826769, 178 | nteeType: 'Arts, Culture and Humanities', 179 | incomeAmount: 22627203, 180 | nteeSuffix: '0', 181 | filingRequirement: '990 (all other) or 990EZ return', 182 | classification: 'Educational Organization', 183 | latest990: 'January, 2021', 184 | rulingDate: 'June, 1983', 185 | nteeCode: 'A51', 186 | groupName: null, 187 | deductibilityCode: '1', 188 | affiliation: 189 | 'Independent - the organization is an independent organization or an independent auxiliary (i.e., not affiliated with a National, Regional, or Geographic grouping of organizations).', 190 | foundationStatus: 191 | 'Organization which receives a substantial part of its support from a governmental unit or the general public 170(b)(1)(A)(vi)', 192 | nteeClassification: 'Art Museums', 193 | accountingPeriod: 'January', 194 | deductibilityDetail: null, 195 | exemptOrgStatus: 'Unconditional Exemption', 196 | exemptOrgStatusCode: '01', 197 | nteeLetter: 'A', 198 | }, 199 | mailingAddress: { 200 | country: null, 201 | stateOrProvince: 'ME', 202 | city: 'Portland', 203 | postalCode: '04101', 204 | streetAddress1: 'Seven Congress Square', 205 | streetAddress2: null, 206 | }, 207 | advisories: { severity: null, active: [Object] }, 208 | organization: { 209 | charityName: 'Portland Museum of Art', 210 | ein: '010378420', 211 | charityNavigatorURL: 212 | 'https://www.charitynavigator.org/?bay=search.summary&orgid=4856&utm_source=DataAPI&utm_content=9989397e', 213 | _rapid_links: [Object], 214 | }, 215 | }, 216 | ]; 217 | const processedData = [ 218 | { 219 | name: 'Farnsworth Art Museum', 220 | mission: 221 | "Celebrating Maine's role in American art, the Farnsworth Art Museum offers a nationally recognized collection of works from many of America's greatest artists. With 20,000 square feet of gallery space and over 10,000 works in the collection, there is always something new on view at the Farnsworth. The museum houses the nation's second-largest collection of works by premier 20th-century sculptor Louise Nevelson. Its Wyeth Center exclusively features works of Andrew, N.C. and Jamie Wyeth. The Farnsworth's library is also housed in its Rockland, ME, campus. Two historic buildings, the Farnsworth Homestead and the Olson House, complete the museum complex.", 222 | link: 'https://www.charitynavigator.org/?bay=search.summary&orgid=7516&utm_source=DataAPI&utm_content=9989397e', 223 | catImage: 224 | 'https://d20umu42aunjpx.cloudfront.net/_gfx_/icons/categories/arts.png?utm_source=DataAPI&utm_content=9989397e', 225 | causeImage: 226 | 'https://d20umu42aunjpx.cloudfront.net/_gfx_/causes/small/museums.gif?utm_source=DataAPI&utm_content=9989397e', 227 | overAllScore: 86.04, 228 | financialRating: undefined, 229 | accountabilityRating: undefined, 230 | deductibility: 'Contributions are deductible', 231 | ein: '010368070', 232 | }, 233 | { 234 | name: 'Portland Museum of Art', 235 | mission: 236 | 'The Portland Museum of Art strives to engage audiences in a dialogue about the relevance of art and culture to our everyday lives and it committed to the stewardship and growth of the collection.', 237 | link: 'https://www.charitynavigator.org/?bay=search.summary&orgid=4856&utm_source=DataAPI&utm_content=9989397e', 238 | catImage: 239 | 'https://d20umu42aunjpx.cloudfront.net/_gfx_/icons/categories/arts.png?utm_source=DataAPI&utm_content=9989397e', 240 | causeImage: 241 | 'https://d20umu42aunjpx.cloudfront.net/_gfx_/causes/small/museums.gif?utm_source=DataAPI&utm_content=9989397e', 242 | overAllScore: 90.67, 243 | financialRating: undefined, 244 | accountabilityRating: undefined, 245 | deductibility: 'Contributions are deductible', 246 | ein: '010378420', 247 | }, 248 | ]; 249 | processCharities(req, res, next); 250 | // console.log(res.locals.parsed); 251 | expect(res.locals.parsed).toEqual(processedData); 252 | }); 253 | }); 254 | --------------------------------------------------------------------------------