├── .gitignore ├── NOTES.md ├── README.md ├── SEEDER_DATA.md ├── WALKTHROUGH.md ├── backend ├── .env.example ├── .eslintrc.json ├── .gitignore ├── .sequelizerc ├── __tests__ │ └── routes.js ├── app.js ├── bin │ └── www ├── config │ ├── database.js │ └── index.js ├── db │ ├── migrations │ │ └── .gitkeep │ ├── models │ │ └── index.js │ ├── seeder-content │ │ └── users.json │ └── seeders │ │ └── 20210407230008-seed-users.js ├── package-lock.json ├── package.json └── routes │ ├── api │ ├── index.js │ └── users.js │ └── index.js └── frontend ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public ├── index.html └── manifest.json └── src ├── App.js ├── assets ├── background1.jpeg ├── fakeUsers.json └── index.js ├── components ├── Button │ ├── Button.js │ ├── Button.module.css │ └── index.js ├── SearchBar │ ├── SearchBar.js │ ├── SearchBar.module.css │ └── index.js ├── UserRow │ ├── UserRow.js │ ├── UserRow.module.css │ └── index.js ├── UsersContainer │ ├── UsersContainer.js │ ├── UsersContainer.module.css │ └── index.js └── __tests__ │ ├── UserRow.js │ └── UsersContainer.js ├── index.css ├── index.js ├── pages └── MainPage │ ├── MainPage.js │ ├── MainPage.module.css │ └── index.js ├── setupTests.js ├── store ├── __tests__ │ └── usersStore.js ├── index.js └── users.js └── utils └── test-utils.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /NOTES.md: -------------------------------------------------------------------------------- 1 | # Things of Note 2 | 3 | Stuff you might want to know about. 4 | 5 | ## Proxy 6 | 7 | * https://create-react-app.dev/docs/proxying-api-requests-in-development 8 | 9 | ## NODE_ENV 10 | 11 | * https://create-react-app.dev/docs/adding-custom-environment-variables 12 | 13 | ## CSS Modules 14 | 15 | * https://github.com/css-modules/css-modules 16 | 17 | ## Normalizing State Shape 18 | 19 | * https://redux.js.org/recipes/structuring-reducers/normalizing-state-shape 20 | * https://redux.js.org/tutorials/essentials/part-6-performance-normalization#normalizing-data 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Data Flow Lecture 2 | 3 | Clone me! 4 | 5 | HTTPS: 6 | ```bash 7 | git clone https://github.com/Lazytangent/DataFlow.git 8 | ``` 9 | 10 | SSH: 11 | ```bash 12 | git clone git@github.com:Lazytangent/DataFlow.git 13 | ``` 14 | 15 | ## Phase 0: Set Up 16 | ### Backend 17 | 18 | 1. `cd` into the repository 19 | 2. `cd` into the `backend` directory. 20 | 3. `npm install` the dependencies. 21 | 4. Make an `.env` file based on the `.env.example` file given. 22 | 5. Run this command to create the user based on the user defined in the `.env` 23 | file. 24 | ```bash 25 | psql -c "CREATE USER data_flow_app with PASSWORD 'password' CREATEDB;" 26 | ``` 27 | 6. Run `npx dotenv sequelize db:create` to create the database. 28 | 29 | ### Frontend 30 | 31 | 1. In another terminal, `cd` into the `frontend` directory. 32 | 2. `npm install` the dependencies. 33 | 34 | ### Start the servers 35 | 36 | In both terminals, run `npm start` to start the servers. 37 | 38 | ## Guided Walkthrough 39 | 40 | Go to the [WALKTHROUGH.md](WALKTHROUGH.md) for the detailed walkthrough. 41 | 42 | ## Things of note 43 | 44 | Go see [NOTES.md](NOTES.md) for some side notes on things you can read more 45 | about. 46 | -------------------------------------------------------------------------------- /SEEDER_DATA.md: -------------------------------------------------------------------------------- 1 | # How to Create Seeder Data for This Project 2 | 3 | There's one seeder file for this project that seeds the one table from the 4 | migration. That seeder file will read for a `users.json` in the 5 | `backend/db/seeder-content` directory. The `users.json` file should contain an 6 | array of "User" objects, and each "User" object should have the following 7 | attributes: 8 | 9 | * username: string 10 | * name: string 11 | * email: string 12 | 13 | There's already a really basic `users.json` in the `backend/db/seeder-content` 14 | directory that you can use to base your data off of. 15 | -------------------------------------------------------------------------------- /WALKTHROUGH.md: -------------------------------------------------------------------------------- 1 | # Walkthrough 2 | 3 | This is meant as a refresher for walking you through the data flow in an 4 | Express-React-Redux application with a PostgreSQL database. (Essentially a PERN 5 | stack). 6 | 7 | In this exercise, we will: 8 | 9 | * Create a User model and migration and apply it to the database 10 | * Create a `GET` route to fetch our users from the database 11 | * Create a reducer for this new feature/slice of state 12 | * Create a thunk that does the fetching from the API route 13 | * Create an action creator that sets our data from the thunk into the Redux 14 | store 15 | * Create an action type as a constant (`const`) to prevent typos being an issue 16 | in our reducer and actions 17 | * Create a case in our reducer that matches the action type we've created 18 | * Dispatch the thunk in our component as a side effect to fetch the data 19 | * Select the data we've put into the store with the fetch from the dispatched 20 | thunk 21 | 22 | ## Table of Contents 23 | 24 | * [Phase 1: Planning] 25 | * [Phase 2: Database] 26 | * [Phase 3: Express] 27 | * [Phase 4: Redux] 28 | * [Phase 5: React] 29 | 30 | ## Phase 1: Planning 31 | 32 | First, you have to plan out what you want. This is when you choose an MVP or 33 | feature to work on, and it's recommended that you work on that feature until it 34 | is complete. For the CRUD (Create, Read, Update, Delete) operations on an MVP, I 35 | would start with the Read (`GET` routes) then do Create (`POST`), Update (`PUT` 36 | or `PATCH`), and Delete (`DELETE`) in that order. That's just how I do it. That 37 | would mean planning out what you want to see on a certain page or set of pages, 38 | so you can figure out what data you'll need from your database. 39 | 40 | For this demo application, we'll be getting all the users and rendering them on 41 | the page. There's already some magic working behind the scenes to filter out 42 | users if the search bar is in use, but that's not the concern of this exercise. 43 | For this exercise, we will: 44 | 45 | * Create a User model and migration 46 | * Create one backend API route to `GET` all the users. 47 | * Create a thunk to `fetch` that API route and dispatch the data from the 48 | response to an action creator. 49 | * Create an action creator to apply the data it receives into the Redux store. 50 | * Create a constant to use as the type of the action creator and be the case in 51 | our reducer. 52 | * Create a case in the reducer. 53 | * Grab the information from the Redux store and render it in our React 54 | component. 55 | 56 | Things we will not be doing in this exercise: 57 | 58 | * Set up the boilerplate code for the backend Express app. 59 | * Set up the boilerplate code for a React app. 60 | * Set up the boilerplate code for connecting Redux to the React app. 61 | 62 | The goals of this exercise are: 63 | 64 | * To familiarize you with the data-flow and how Redux sits between the React 65 | application in the frontend and the Express application in the backend. 66 | * To familiarize you with some of the patterns used when setting data into and 67 | getting data out of the Redux store. 68 | 69 | ### NOTE 70 | 71 | You DO NOT have to follow this walkthrough exactly. Some people prefer working 72 | from the component to the Redux store to the backend, back to the Redux store, 73 | and then finally back to the component. Some people prefer working from the 74 | backend to the Redux store then to the component. Neither one is better than the 75 | other, it's just a personal preference in how you work. 76 | 77 | That said, since this walkthrough is done from the backend with creating and 78 | testing the API route first, if you do want to use this walkthrough to work from 79 | the frontend with the component first, it is suggested to read through all of 80 | this walkthrough first before doing anything since it'll make more sense when 81 | all put together. Otherwise, code along! 82 | 83 | ## Phase 2: Database 84 | 85 | First, we'll set up a User model and migration to store our user data. In your 86 | project, you'll probably be only doing database setup at the very beginning of 87 | your project or as you start working on another MVP or general feature that 88 | needs another table. 89 | 90 | For the purposes of this exercise, run the following command to create a User 91 | model with the right columns: 92 | 93 | ```sh 94 | npx sequelize model:generate --name User --attributes "name:string, \ 95 | username:string, email:string" 96 | ``` 97 | 98 | To set up your migration and model correctly, be sure to add the necessary 99 | validations now so, at the very least, you'll have the model and migration 100 | validations. 101 | 102 | For our User migration, update your migration file to this: 103 | 104 | ```js 105 | 'use strict'; 106 | module.exports = { 107 | up: (queryInterface, Sequelize) => { 108 | return queryInterface.createTable('Users', { 109 | id: { 110 | allowNull: false, 111 | autoIncrement: true, 112 | primaryKey: true, 113 | type: Sequelize.INTEGER, 114 | }, 115 | name: { 116 | type: Sequelize.STRING, 117 | allowNull: false, 118 | }, 119 | email: { 120 | type: Sequelize.STRING, 121 | allowNull: false, 122 | unique: true, 123 | }, 124 | username: { 125 | type: Sequelize.STRING, 126 | allowNull: false, 127 | unique: true, 128 | }, 129 | createdAt: { 130 | allowNull: false, 131 | type: Sequelize.DATE, 132 | defaultValue: Sequelize.fn('now'), 133 | }, 134 | updatedAt: { 135 | allowNull: false, 136 | type: Sequelize.DATE, 137 | defaultValue: Sequelize.fn('now'), 138 | } 139 | }); 140 | }, 141 | down: (queryInterface, Sequelize) => { 142 | return queryInterface.dropTable('Users'); 143 | } 144 | }; 145 | ``` 146 | 147 | For our User model, update your model file to this: 148 | 149 | ```js 150 | "use strict"; 151 | module.exports = (sequelize, DataTypes) => { 152 | const User = sequelize.define( 153 | "User", 154 | { 155 | name: { 156 | type: DataTypes.STRING, 157 | allowNull: false, 158 | }, 159 | email: { 160 | type: DataTypes.STRING, 161 | allowNull: false, 162 | unique: true, 163 | }, 164 | username: { 165 | type: DataTypes.STRING, 166 | allowNull: false, 167 | unique: true, 168 | }, 169 | }, 170 | {} 171 | ); 172 | User.associate = function (models) { 173 | // associations can be defined here 174 | }; 175 | return User; 176 | }; 177 | ``` 178 | 179 | After modifying your model and migration with the proper constraints (and 180 | possibly associations, as well), you can run the `db:migrate` command to apply 181 | the migration to your database. 182 | 183 | ```sh 184 | npx dotenv sequelize db:migrate 185 | ``` 186 | 187 | For the purposes of this exercise, let's now also run the command to seed our 188 | Users table. 189 | 190 | ```sh 191 | npx dotenv sequelize db:seed:all 192 | ``` 193 | 194 | ## Phase 3: Express 195 | 196 | To complete the backend section of the data flow, you'll need to create the API 197 | route that you will have respond with the appropriate information. In the case 198 | of this example, this API endpoint will need to be a HTTP `GET` request to the 199 | route `/api/users`. 200 | 201 | Things you'll want to remember: 202 | 203 | * Since you're going to be querying data from the database, you'll need to 204 | import the model that you'll be using. 205 | * Because there is a database interaction, there will need to be some kind of handler 206 | that covers the asynchronous nature of database interactions. 207 | * An endpoint also needs to do something with a response to complete the 208 | request-response cycle. Since this is an API route that just handles data, 209 | which method on the response should we use? 210 | 211 | ### Testing your API route 212 | 213 | **Make sure your backend server is running before you try 214 | testing the route!** 215 | 216 | Once you've set up your API route, you can test it with [Postman] or [Insomnia]. 217 | If you're testing with either in your actual application with authentication, 218 | make sure you remember to add the proper authentication tokens, which depends on 219 | what you're using in that app. 220 | 221 | For this exercise, we'll send a `GET` request to 222 | `http://localhost:5000/api/users`, and since there's no authentication involved, 223 | there's nothing we'll need to add to our headers to get things working once the 224 | API route is written. 225 | 226 | ## Phase 4: Redux 227 | 228 | To complete and test the Redux portion of the data flow process, you'll need to 229 | do a few things: 230 | 231 | * Create an action type constant 232 | * Create an action creator that returns an action (just a POJO) 233 | * Create a thunk that dispatches the action creator 234 | * Create a reducer and add it to the store 235 | * Create a case that matches the action type and returns a new state 236 | 237 | ### Testing your actions 238 | 239 | **Make sure both your frontend and backend servers are running before you try 240 | testing the actions!** 241 | 242 | Since the configuration for connecting the Redux store and the actions is 243 | already done in the `src/index.js` file, you can simply test the Redux thunk 244 | you've written for this exercise by running something similar in the browser. 245 | 246 | ```javascript 247 | window.store.dispatch(window.userActions.getUsers()); 248 | ``` 249 | 250 | Because we've attached the `store` and `userActions` to the window object during 251 | development, we can access those properties by keying into the window object and 252 | dispatching the action manually. You can test your thunks (and action creators) 253 | like so by making sure you've exported them properly from the store where 254 | they're defined and imported them into `src/index.js` and attached them properly 255 | to the window object. 256 | 257 | ## Phase 5: React 258 | 259 | To complete this portion of the data flow process, you'll need to dispatch a 260 | thunk to fetch the information and then render the information from the Redux 261 | store in a component. 262 | 263 | ### Testing your components 264 | 265 | **Make sure both your frontend and backend servers are running before you try to 266 | test your components!** 267 | 268 | To thoroughly test your components and make sure that they render the 269 | appropriate data every time, you'll want to test that your component works as 270 | intended: 271 | 272 | * After a refresh of the browser, to simulate a user coming to that page from 273 | outside your site and with the Redux store starting off from the initial 274 | state. 275 | * After coming from other pages from your app, where the Redux store may or may 276 | not be pre-populated with information since there may be overlapping 277 | information that you don't want to appear in the component you're working 278 | on. 279 | 280 | Making sure that your component renders the correct information consistently, no 281 | matter what page your user is coming from is important to creating a smooth and 282 | predictable user experience. 283 | 284 | [Phase 1: Planning]: #phase-1-planning 285 | [Phase 2: Database]: #phase-2-database 286 | [Phase 3: Express]: #phase-3-express 287 | [Phase 4: Redux]: #phase-4-redux 288 | [Phase 5: React]: #phase-5-react 289 | 290 | [Postman]: https://www.postman.com/ 291 | [Insomnia]: https://insomnia.rest/ 292 | -------------------------------------------------------------------------------- /backend/.env.example: -------------------------------------------------------------------------------- 1 | PORT=5000 2 | DB_USERNAME=data_flow_app 3 | DB_PASSWORD=password 4 | DB_DATABASE=data_flow_db 5 | DB_HOST=localhost 6 | -------------------------------------------------------------------------------- /backend/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true, 5 | "es2021": true, 6 | "jest/globals": true 7 | }, 8 | "extends": "eslint:recommended", 9 | "parserOptions": { 10 | "ecmaVersion": 12 11 | }, 12 | "rules": { 13 | "jest/no-disabled-tests": "warn", 14 | "jest/no-focused-tests": "error", 15 | "jest/no-identical-title": "error", 16 | "jest/prefer-to-have-length": "warn", 17 | "jest/valid-expect": "error" 18 | }, 19 | "plugins": ["jest"] 20 | } 21 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | sqlite 2 | 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # Bower dependency directory (https://bower.io/) 29 | bower_components 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Compiled binary addons (https://nodejs.org/api/addons.html) 35 | build/Release 36 | 37 | # Dependency directories 38 | node_modules/ 39 | jspm_packages/ 40 | 41 | # Typescript v1 declaration files 42 | typings/ 43 | 44 | # Optional npm cache directory 45 | .npm 46 | 47 | # Optional eslint cache 48 | .eslintcache 49 | 50 | # Optional REPL history 51 | .node_repl_history 52 | 53 | # Output of 'npm pack' 54 | *.tgz 55 | 56 | # Yarn Integrity file 57 | .yarn-integrity 58 | 59 | # dotenv environment variables file 60 | .env 61 | 62 | # next.js build output 63 | .next 64 | -------------------------------------------------------------------------------- /backend/.sequelizerc: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | config: path.resolve('config', 'database.js'), 5 | 'models-path': path.resolve('db', 'models'), 6 | 'seeders-path': path.resolve('db', 'seeders'), 7 | 'migrations-path': path.resolve('db', 'migrations'), 8 | }; 9 | -------------------------------------------------------------------------------- /backend/__tests__/routes.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest'); 2 | const app = require('../app'); 3 | const { sequelize, User } = require('../db/models'); 4 | 5 | describe('Get Endpoints', () => { 6 | it('should get a message', async () => { 7 | const res = await request(app) 8 | .get('/api/test') 9 | .expect('Content-Type', /json/) 10 | .expect(200) 11 | expect(res.body).toHaveProperty('message'); 12 | }); 13 | }); 14 | 15 | beforeAll(async () => { 16 | await sequelize.sync({ force: true, logging: false }); 17 | }); 18 | 19 | afterAll(async () => { 20 | await sequelize.close(); 21 | }); 22 | 23 | describe('GET /api/users', () => { 24 | it('should exist', async () => { 25 | await request(app) 26 | .get('/api/users') 27 | .expect(200) 28 | }); 29 | 30 | it('should return JSON', async () => { 31 | await request(app) 32 | .get('/api/users') 33 | .expect('Content-Type', /json/) 34 | .expect(200) 35 | }); 36 | 37 | it('should return all the users in the database', async () => { 38 | const fakeUser1 = { 39 | name: "Demo Tester", 40 | email: "demo@aa.io", 41 | username: "demoman", 42 | }; 43 | const fakeUser2 = { 44 | name: "Test Demoer", 45 | email: "test@aa.io", 46 | username: "testman", 47 | }; 48 | 49 | await User.create(fakeUser1); 50 | await User.create(fakeUser2); 51 | 52 | const res = await request(app) 53 | .get('/api/users') 54 | .expect(200) 55 | 56 | expect(res.body).toEqual(expect.arrayContaining([ 57 | expect.objectContaining(fakeUser1), 58 | expect.objectContaining(fakeUser2), 59 | ])); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /backend/app.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const cookieParser = require("cookie-parser"); 3 | const morgan = require("morgan"); 4 | const helmet = require("helmet"); 5 | 6 | const router = require('./routes'); 7 | 8 | const app = express(); 9 | 10 | app.use(morgan("dev")); 11 | app.use(express.json()); 12 | app.use(cookieParser()); 13 | 14 | app.use( 15 | helmet({ 16 | contentSecurityPolicy: false, 17 | }) 18 | ); 19 | 20 | app.use(router); 21 | 22 | app.get("/api/test", (_req, res) => { 23 | res.json({ message: "Test route... for testing" }); 24 | }); 25 | 26 | app.use((_req, _res, next) => { 27 | const err = new Error("The requested resource couldn't be found."); 28 | err.title = "Resource Not Found"; 29 | err.errors = ["The requested resource couldn't be found."]; 30 | err.status = 404; 31 | next(err); 32 | }); 33 | 34 | app.use((err, _req, res, _next) => { 35 | res.status(err.status || 500); 36 | console.error(err); 37 | res.json({ 38 | title: err.title || "Server Error", 39 | message: err.message, 40 | errors: err.errors, 41 | stack: err.stack, 42 | }); 43 | }); 44 | 45 | module.exports = app; 46 | -------------------------------------------------------------------------------- /backend/bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const { port } = require('../config'); 4 | 5 | const app = require('../app'); 6 | const db = require('../db/models'); 7 | 8 | db.sequelize 9 | .authenticate() 10 | .then(() => { 11 | console.log('Database connection success! Sequelize is ready to use...'); 12 | app.listen(port, () => console.log(`Listening on port ${port}...`)); 13 | }) 14 | .catch((err) => { 15 | console.log('Database connection failure.'); 16 | console.error(err); 17 | }); 18 | -------------------------------------------------------------------------------- /backend/config/database.js: -------------------------------------------------------------------------------- 1 | const config = require('./index'); 2 | 3 | const db = config.db; 4 | const username = db.username; 5 | const password = db.password; 6 | const database = db.database; 7 | const host = db.host; 8 | 9 | module.exports = { 10 | development: { 11 | username, 12 | password, 13 | database, 14 | host, 15 | dialect: 'postgres', 16 | seederStorage: 'sequelize', 17 | }, 18 | production: { 19 | use_env_variable: 'DATABASE_URL', 20 | dialect: 'postgres', 21 | seederStorage: 'sequelize', 22 | dialectOptions: { 23 | ssl: { 24 | require: true, 25 | rejectUnauthorized: false, 26 | }, 27 | }, 28 | }, 29 | test: { 30 | dialect: 'sqlite', 31 | storage: ':memory:', 32 | logging: false, 33 | }, 34 | }; 35 | -------------------------------------------------------------------------------- /backend/config/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | environment: process.env.NODE_ENV || 'development', 3 | port: process.env.PORT || 5000, 4 | db: { 5 | username: process.env.DB_USERNAME, 6 | password: process.env.DB_PASSWORD, 7 | database: process.env.DB_DATABASE, 8 | host: process.env.DB_HOST, 9 | test: { 10 | username: process.env.TEST_DB_USERNAME, 11 | password: process.env.TEST_DB_PASSWORD, 12 | database: process.env.TEST_DB_DATABASE, 13 | host: process.env.TEST_DB_HOST, 14 | }, 15 | }, 16 | jwtConfig: { 17 | secret: process.env.JWT_SECRET, 18 | expiresIn: process.env.JWT_EXPIRES_IN, 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /backend/db/migrations/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lazytangent/DataFlow/0b337b33abfb47ec81fa1372adca7d7512bfebad/backend/db/migrations/.gitkeep -------------------------------------------------------------------------------- /backend/db/models/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const Sequelize = require('sequelize'); 6 | const basename = path.basename(__filename); 7 | const env = process.env.NODE_ENV || 'development'; 8 | const config = require(__dirname + '/../../config/database.js')[env]; 9 | const db = {}; 10 | 11 | let sequelize; 12 | if (config.use_env_variable) { 13 | sequelize = new Sequelize(process.env[config.use_env_variable], config); 14 | } else { 15 | sequelize = new Sequelize(config.database, config.username, config.password, config); 16 | } 17 | 18 | fs 19 | .readdirSync(__dirname) 20 | .filter(file => { 21 | return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js'); 22 | }) 23 | .forEach(file => { 24 | const model = sequelize['import'](path.join(__dirname, file)); 25 | db[model.name] = model; 26 | }); 27 | 28 | Object.keys(db).forEach(modelName => { 29 | if (db[modelName].associate) { 30 | db[modelName].associate(db); 31 | } 32 | }); 33 | 34 | db.sequelize = sequelize; 35 | db.Sequelize = Sequelize; 36 | 37 | module.exports = db; 38 | -------------------------------------------------------------------------------- /backend/db/seeder-content/users.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "username": "DatabaseTest", 4 | "name": "Database Test", 5 | "email": "database@test.io" 6 | } 7 | ] 8 | -------------------------------------------------------------------------------- /backend/db/seeders/20210407230008-seed-users.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const data = require("../seeder-content/users.json"); 3 | 4 | module.exports = { 5 | up: (queryInterface, Sequelize) => { 6 | const users = []; 7 | for (const user of data) { 8 | users.push(user); 9 | } 10 | 11 | return queryInterface.bulkInsert('Users', users, {}); 12 | }, 13 | 14 | down: (queryInterface, Sequelize) => { 15 | return queryInterface.bulkDelete('Users', null, { 16 | truncate: true, 17 | restartIdentity: true, 18 | }); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "sequelize": "sequelize", 7 | "sequelize-cli": "sequelize-cli", 8 | "start": "per-env", 9 | "start:development": "nodemon -r dotenv/config ./bin/www", 10 | "start:production": "node ./bin/www", 11 | "test": "cross-env NODE_ENV=test jest --testTimeout=10000 --watch", 12 | "migrate": "npx dotenv sequelize-cli db:migrate", 13 | "migrate:reset": "npx dotenv sequelize db:migrate:undo:all && npm run migrate" 14 | }, 15 | "dependencies": { 16 | "cookie-parser": "~1.4.4", 17 | "debug": "~2.6.9", 18 | "dotenv": "^8.2.0", 19 | "express": "~4.16.1", 20 | "express-async-handler": "^1.1.4", 21 | "helmet": "^4.4.1", 22 | "morgan": "^1.9.1", 23 | "per-env": "^1.0.2", 24 | "pg": "^8.5.1", 25 | "sequelize": "^5.22.4", 26 | "sequelize-cli": "^5.5.1" 27 | }, 28 | "devDependencies": { 29 | "cross-env": "^7.0.3", 30 | "dotenv-cli": "^4.0.0", 31 | "eslint": "^7.24.0", 32 | "eslint-plugin-jest": "^24.3.4", 33 | "jest": "^26.6.3", 34 | "nodemon": "^2.0.7", 35 | "sqlite3": "^5.0.2", 36 | "supertest": "^6.1.3" 37 | }, 38 | "jest": { 39 | "testEnvironment": "node", 40 | "coveragePathIgnorePatterns": [ 41 | "/node_modules/" 42 | ] 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /backend/routes/api/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | // Import your router here 3 | 4 | const router = express.Router(); 5 | 6 | // Use your router here 7 | 8 | module.exports = router; 9 | -------------------------------------------------------------------------------- /backend/routes/api/users.js: -------------------------------------------------------------------------------- 1 | // Create a router here 2 | // Since we're doing database stuff, you'll want some kind of asyncHandler 3 | 4 | // Take a second to import the database stuff you'll need 5 | // Here's where you'd also import other middleware 6 | 7 | // Create the API route here 8 | 9 | // Remember to export the router, too 10 | -------------------------------------------------------------------------------- /backend/routes/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const apiRouter = require('./api'); 3 | 4 | const router = express.Router(); 5 | 6 | router.use('/api', apiRouter); 7 | 8 | module.exports = router; 9 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | .eslintcache 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Create React App Template 3 | 4 | A no-frills template from which to create React applications with 5 | [Create React App](https://github.com/facebook/create-react-app). 6 | 7 | ```sh 8 | npx create-react-app my-app --template @appacademy/simple --use-npm 9 | ``` 10 | 11 | ## Available Scripts 12 | 13 | In the project directory, you can run: 14 | 15 | ### `npm start` 16 | 17 | Runs the app in the development mode.\ 18 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 19 | 20 | The page will reload if you make edits.\ 21 | You will also see any lint errors in the console. 22 | 23 | ### `npm test` 24 | 25 | Launches the test runner in the interactive watch mode.\ 26 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 27 | 28 | ### `npm run build` 29 | 30 | Builds the app for production to the `build` folder.\ 31 | It correctly bundles React in production mode and optimizes the build for the best performance. 32 | 33 | The build is minified and the filenames include the hashes.\ 34 | Your app is ready to be deployed! 35 | 36 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 37 | 38 | ### `npm run eject` 39 | 40 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 41 | 42 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 43 | 44 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 45 | 46 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 47 | 48 | ## Learn More 49 | 50 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 51 | 52 | To learn React, check out the [React documentation](https://reactjs.org/). 53 | 54 | ### Code Splitting 55 | 56 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) 57 | 58 | ### Analyzing the Bundle Size 59 | 60 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) 61 | 62 | ### Making a Progressive Web App 63 | 64 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) 65 | 66 | ### Advanced Configuration 67 | 68 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) 69 | 70 | ### Deployment 71 | 72 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) 73 | 74 | ### `npm run build` fails to minify 75 | 76 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) 77 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.11.10", 7 | "@testing-library/react": "^11.2.6", 8 | "@testing-library/user-event": "^12.8.3", 9 | "normalize.css": "^8.0.1", 10 | "react": "^17.0.2", 11 | "react-dom": "^17.0.2", 12 | "react-redux": "^7.2.3", 13 | "react-router-dom": "^5.2.0", 14 | "react-scripts": "^4.0.3", 15 | "redux": "^4.0.5", 16 | "redux-thunk": "^2.3.0" 17 | }, 18 | "scripts": { 19 | "start": "react-scripts start", 20 | "build": "react-scripts build", 21 | "test": "react-scripts test", 22 | "eject": "react-scripts eject" 23 | }, 24 | "eslintConfig": { 25 | "extends": "react-app" 26 | }, 27 | "browserslist": { 28 | "production": [ 29 | ">0.2%", 30 | "not dead", 31 | "not op_mini all" 32 | ], 33 | "development": [ 34 | "last 1 chrome version", 35 | "last 1 firefox version", 36 | "last 1 safari version" 37 | ] 38 | }, 39 | "devDependencies": { 40 | "fetch-mock": "^9.11.0", 41 | "node-fetch": "^2.6.1", 42 | "redux-logger": "^3.0.6", 43 | "redux-mock-store": "^1.5.4" 44 | }, 45 | "proxy": "http://localhost:5000" 46 | } 47 | -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Dataflow Demo 9 | 10 | 11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Dataflow Demo", 3 | "name": "Dataflow Demo", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "theme_color": "#000000", 7 | "background_color": "#ffffff" 8 | } 9 | -------------------------------------------------------------------------------- /frontend/src/App.js: -------------------------------------------------------------------------------- 1 | import MainPage from './pages/MainPage'; 2 | 3 | const App = () => { 4 | return ( 5 | <> 6 | 7 | 8 | ); 9 | } 10 | 11 | export default App; 12 | -------------------------------------------------------------------------------- /frontend/src/assets/background1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lazytangent/DataFlow/0b337b33abfb47ec81fa1372adca7d7512bfebad/frontend/src/assets/background1.jpeg -------------------------------------------------------------------------------- /frontend/src/assets/fakeUsers.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 0, 4 | "name": "Test User", 5 | "email": "test@example.com" 6 | } 7 | ] 8 | -------------------------------------------------------------------------------- /frontend/src/assets/index.js: -------------------------------------------------------------------------------- 1 | import fakeUsers from './fakeUsers.json'; 2 | 3 | export { 4 | fakeUsers, 5 | }; 6 | -------------------------------------------------------------------------------- /frontend/src/components/Button/Button.js: -------------------------------------------------------------------------------- 1 | import styles from './Button.module.css'; 2 | 3 | const Button = ({ label, onClick }) => { 4 | return ( 5 | 6 | ); 7 | }; 8 | 9 | export default Button; 10 | -------------------------------------------------------------------------------- /frontend/src/components/Button/Button.module.css: -------------------------------------------------------------------------------- 1 | .button { 2 | padding: 0.35rem; 3 | } 4 | -------------------------------------------------------------------------------- /frontend/src/components/Button/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './Button'; 2 | -------------------------------------------------------------------------------- /frontend/src/components/SearchBar/SearchBar.js: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { useHistory } from 'react-router-dom'; 3 | 4 | import styles from './SearchBar.module.css'; 5 | 6 | const SearchBar = () => { 7 | const history = useHistory(); 8 | const [queryString, setQueryString] = useState(new URLSearchParams(history.location.search).get('q') ?? ''); 9 | 10 | const updateSearch = (e) => { 11 | setQueryString(e.target.value); 12 | if (e.target.value) { 13 | history.replace({ 14 | pathname: '/', 15 | search: `?q=${e.target.value}`, 16 | }); 17 | } else { 18 | history.replace({ 19 | pathname: '', 20 | }); 21 | } 22 | }; 23 | 24 | return ( 25 |
26 | 27 |
28 | ); 29 | }; 30 | 31 | export default SearchBar; 32 | -------------------------------------------------------------------------------- /frontend/src/components/SearchBar/SearchBar.module.css: -------------------------------------------------------------------------------- 1 | .searchBar { 2 | padding: 0.5rem; 3 | font-size: 1.5rem; 4 | outline: none; 5 | border-radius: 0.25rem; 6 | border: thin solid black; 7 | } 8 | 9 | .searchBar:focus { 10 | border: thin solid blue; 11 | } 12 | 13 | .div { 14 | width: fit-content; 15 | margin: auto; 16 | margin-bottom: 0.5rem; 17 | } 18 | -------------------------------------------------------------------------------- /frontend/src/components/SearchBar/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './SearchBar'; 2 | -------------------------------------------------------------------------------- /frontend/src/components/UserRow/UserRow.js: -------------------------------------------------------------------------------- 1 | import { useLocation } from 'react-router-dom'; 2 | 3 | const UserRow = ({ user }) => { 4 | const queryString = new URLSearchParams(useLocation().search).get('q') ?? ''; 5 | 6 | if (!(user.name.includes(queryString) || user.email.includes(queryString) || String(user.id).includes(queryString))) { 7 | return null; 8 | } 9 | 10 | return ( 11 | 12 | {user.id} 13 | {user.name} 14 | {user.email} 15 | 16 | ); 17 | }; 18 | 19 | export default UserRow; 20 | -------------------------------------------------------------------------------- /frontend/src/components/UserRow/UserRow.module.css: -------------------------------------------------------------------------------- 1 | .card { 2 | padding: 0.5rem; 3 | border-radius: 0.25rem; 4 | border: thin solid gray; 5 | width: fit-content; 6 | margin: auto; 7 | } 8 | -------------------------------------------------------------------------------- /frontend/src/components/UserRow/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './UserRow'; 2 | -------------------------------------------------------------------------------- /frontend/src/components/UsersContainer/UsersContainer.js: -------------------------------------------------------------------------------- 1 | // Import hooks from 'react'. Which hook is meant for causing side effects? 2 | // Import hooks from 'react-redux' 3 | 4 | // Import the thunk creator 5 | import styles from './UsersContainer.module.css'; 6 | import { fakeUsers } from '../../assets'; 7 | import UserRow from '../UserRow'; 8 | 9 | const UsersContainer = () => { 10 | // Declare variables from hooks 11 | 12 | // Use a 'react' hook and cause a side effect 13 | 14 | return ( 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | {fakeUsers.map((user) => )} 26 | 27 |
User No.User's NameUser's Email
28 |
29 | ); 30 | }; 31 | 32 | export default UsersContainer; 33 | -------------------------------------------------------------------------------- /frontend/src/components/UsersContainer/UsersContainer.module.css: -------------------------------------------------------------------------------- 1 | .tableContainer { 2 | width: 75%; 3 | margin: auto; 4 | display: flex; 5 | border-radius: 0.35rem; 6 | justify-content: center; 7 | 8 | backdrop-filter: grayscale(0.5) blur(5px); 9 | -webkit-backdrop-filter: grayscale(0.5) blur(5px); 10 | } 11 | 12 | .table { 13 | width: 100%; 14 | padding: 0.35rem; 15 | border-radius: 0.25rem; 16 | } 17 | 18 | .thead th { 19 | border-bottom: thin solid black; 20 | padding: 0.35rem; 21 | } 22 | 23 | .tbody td { 24 | padding: 0.35rem; 25 | } 26 | -------------------------------------------------------------------------------- /frontend/src/components/UsersContainer/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './UsersContainer'; 2 | -------------------------------------------------------------------------------- /frontend/src/components/__tests__/UserRow.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from "../../utils/test-utils"; 2 | import UserRow from "../UserRow"; 3 | 4 | describe("The UserRow component", () => { 5 | describe("renders", () => { 6 | beforeEach(() => { 7 | const user = { 8 | id: 0, 9 | name: "Test User", 10 | email: "test@aa.io", 11 | }; 12 | 13 | render( 14 | 15 | 16 | 17 | 18 |
19 | ); 20 | }); 21 | 22 | test("the user's id", () => { 23 | const id = screen.getByText("0"); 24 | expect(id).toHaveTextContent("0"); 25 | }); 26 | 27 | test("the user's name", () => { 28 | const name = screen.getByText("Test User"); 29 | expect(name).toHaveTextContent("Test User"); 30 | }); 31 | 32 | test("the user's email", () => { 33 | const email = screen.getByText("test@aa.io"); 34 | expect(email).toHaveTextContent("test@aa.io"); 35 | }); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /frontend/src/components/__tests__/UsersContainer.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from "../../utils/test-utils"; 2 | import fetchMock from "fetch-mock"; 3 | 4 | import UsersContainer from "../UsersContainer"; 5 | 6 | const users = [ 7 | { 8 | id: 1, 9 | name: "Test User", 10 | email: "test@aa.io", 11 | }, 12 | { 13 | id: 2, 14 | name: "UserTest", 15 | email: "test@user.io", 16 | }, 17 | ]; 18 | 19 | describe("The UsersContainer component", () => { 20 | describe("dispatches a thunk", () => { 21 | beforeEach(() => { 22 | fetchMock.getOnce("/api/users", { 23 | body: users, 24 | headers: { "Content-Type": "application/json" }, 25 | }); 26 | 27 | render( 28 | 29 | ); 30 | }); 31 | 32 | afterEach(() => { 33 | fetchMock.restore(); 34 | }); 35 | 36 | test("and calls the GET /api/users API route", () => { 37 | const result = fetchMock.called("/api/users"); 38 | expect(result).toBe(true); 39 | }); 40 | }); 41 | 42 | describe("renders", () => { 43 | beforeEach(() => { 44 | fetchMock.getOnce("/api/users", { 45 | body: users, 46 | headers: { "Content-Type": "application/json" }, 47 | }); 48 | 49 | render( 50 | , 51 | ); 52 | }); 53 | 54 | afterEach(() => { 55 | fetchMock.restore(); 56 | }); 57 | 58 | test("the test users' ids", () => { 59 | const userOneId = screen.getByText("1"); 60 | const userTwoId = screen.getByText("2"); 61 | expect(userOneId).toHaveTextContent("1"); 62 | expect(userTwoId).toHaveTextContent("2"); 63 | }); 64 | 65 | test("the test users' names", () => { 66 | const userOneName = screen.getByText("Test User"); 67 | const userTwoName = screen.getByText("UserTest"); 68 | expect(userOneName).toHaveTextContent("Test User"); 69 | expect(userTwoName).toHaveTextContent("UserTest"); 70 | }); 71 | 72 | test("the test users' emails", () => { 73 | const userOneEmail = screen.getByText("test@aa.io"); 74 | const userTwoEmail = screen.getByText("test@user.io"); 75 | expect(userOneEmail).toHaveTextContent("test@aa.io"); 76 | expect(userTwoEmail).toHaveTextContent("test@user.io"); 77 | }); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /frontend/src/index.css: -------------------------------------------------------------------------------- 1 | @import-normalize; 2 | 3 | html { 4 | font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; 5 | background-image: url(./assets/background1.jpeg); 6 | background-size: cover; 7 | } 8 | -------------------------------------------------------------------------------- /frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | import { BrowserRouter } from 'react-router-dom'; 5 | 6 | import './index.css'; 7 | import App from './App'; 8 | import configureStore from './store'; 9 | import * as userActions from './store/users'; 10 | 11 | const store = configureStore(); 12 | 13 | if (process.env.NODE_ENV !== 'production') { 14 | window.store = store; 15 | window.userActions = userActions; 16 | } 17 | 18 | const Root = () => ( 19 | 20 | 21 | 22 | 23 | 24 | ); 25 | 26 | ReactDOM.render( 27 | 28 | 29 | , 30 | document.getElementById('root') 31 | ); 32 | -------------------------------------------------------------------------------- /frontend/src/pages/MainPage/MainPage.js: -------------------------------------------------------------------------------- 1 | import styles from './MainPage.module.css'; 2 | import SearchBar from '../../components/SearchBar'; 3 | import UsersContainer from '../../components/UsersContainer'; 4 | 5 | const MainPage = () => { 6 | return ( 7 | <> 8 |

Search for Users

9 | 10 | 11 | 12 | ); 13 | }; 14 | 15 | export default MainPage; 16 | -------------------------------------------------------------------------------- /frontend/src/pages/MainPage/MainPage.module.css: -------------------------------------------------------------------------------- 1 | .heading { 2 | display: flex; 3 | justify-content: center; 4 | font-size: 3rem; 5 | } 6 | -------------------------------------------------------------------------------- /frontend/src/pages/MainPage/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './MainPage'; 2 | -------------------------------------------------------------------------------- /frontend/src/setupTests.js: -------------------------------------------------------------------------------- 1 | import "@testing-library/jest-dom"; 2 | -------------------------------------------------------------------------------- /frontend/src/store/__tests__/usersStore.js: -------------------------------------------------------------------------------- 1 | import configureMockStore from "redux-mock-store"; 2 | import thunk from "redux-thunk"; 3 | import fetchMock from "fetch-mock"; 4 | 5 | import usersReducer, * as userActions from "../users"; 6 | 7 | const middlewares = [thunk]; 8 | const mockStore = configureMockStore(middlewares); 9 | const users = [ 10 | { 11 | id: 1, 12 | name: "Test User", 13 | email: "test@aa.io", 14 | }, 15 | { 16 | id: 2, 17 | name: "UserTest", 18 | email: "test@user.io", 19 | }, 20 | ]; 21 | 22 | // Defining types here since they don't get exported 23 | const SET_USERS = "users/setUsers"; 24 | 25 | describe("The thunk", () => { 26 | beforeEach(() => { 27 | fetchMock.getOnce("/api/users", { 28 | body: users, 29 | headers: { "Content-Type": "application/json" }, 30 | }); 31 | }); 32 | 33 | afterEach(() => { 34 | fetchMock.restore(); 35 | }); 36 | 37 | it("should call GET /api/users at least once", () => { 38 | const store = mockStore({ users: {} }); 39 | store.dispatch(userActions.getUsers()).then(() => { 40 | const result = fetchMock.called("/api/users"); 41 | expect(result).toBe(true); 42 | }); 43 | }); 44 | 45 | it("should create SET_USERS when fetching users has been done", () => { 46 | const expectedActions = [{ type: SET_USERS, users }]; 47 | const store = mockStore({ users: {} }); 48 | 49 | return store.dispatch(userActions.getUsers()).then(() => { 50 | expect(store.getActions()).toEqual(expectedActions); 51 | }); 52 | }); 53 | }); 54 | 55 | describe("The usersReducer", () => { 56 | it("should return the initial state", () => { 57 | expect(usersReducer(undefined, {})).toEqual({}); 58 | }); 59 | 60 | it("should handle SET_USERS", () => { 61 | expect( 62 | usersReducer( 63 | {}, 64 | { 65 | type: SET_USERS, 66 | users, 67 | } 68 | ) 69 | ).toEqual({ 70 | 1: users[0], 71 | 2: users[1], 72 | }); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /frontend/src/store/index.js: -------------------------------------------------------------------------------- 1 | import { createStore, combineReducers, applyMiddleware, compose } from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | 4 | // Import your reducers here 5 | 6 | export const rootReducer = combineReducers({ 7 | // Add your reducers to your store here 8 | }); 9 | 10 | let enhancer; 11 | 12 | if (process.env.NODE_ENV === 'production') { 13 | enhancer = applyMiddleware(thunk); 14 | } else { 15 | const logger = require('redux-logger').default; 16 | const composeEnhancers = 17 | window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; 18 | enhancer = composeEnhancers(applyMiddleware(thunk, logger)); 19 | } 20 | 21 | const configureStore = (preloadedState) => { 22 | return createStore(rootReducer, preloadedState, enhancer); 23 | }; 24 | 25 | export default configureStore; 26 | -------------------------------------------------------------------------------- /frontend/src/store/users.js: -------------------------------------------------------------------------------- 1 | // Define Action Types as Constants 2 | 3 | // Define Action Creators 4 | 5 | // Define Thunks 6 | 7 | // Define an initial state 8 | 9 | // Define a reducer 10 | 11 | // Export the reducer 12 | -------------------------------------------------------------------------------- /frontend/src/utils/test-utils.js: -------------------------------------------------------------------------------- 1 | import { render as rtlRender } from '@testing-library/react'; 2 | import { createStore, compose, applyMiddleware } from 'redux'; 3 | import { Provider } from 'react-redux'; 4 | import { BrowserRouter } from 'react-router-dom'; 5 | import thunk from 'redux-thunk'; 6 | 7 | import { rootReducer } from '../store/'; 8 | 9 | const configureStore = (preloadedState) => 10 | createStore(rootReducer, preloadedState, compose(applyMiddleware(thunk))); 11 | 12 | function render(ui, { initialState, store = configureStore(initialState), ...renderOptions } = {}) { 13 | function Wrapper({ children }) { 14 | return {children}; 15 | } 16 | return rtlRender(ui, { wrapper: Wrapper, ...renderOptions }); 17 | } 18 | 19 | export * from '@testing-library/react'; 20 | export { render }; 21 | --------------------------------------------------------------------------------