20 | FAVORITES
21 | div>
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 |
23 |
PREVIOUS DONATIONS
24 | {charities}
25 | div>
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------