├── .gitignore
├── README.md
├── part0
├── 0.4_new_note.png
├── 0.5_get_single_page_app.png
├── 0.6_single_page_app_new_note.png
└── README.md
├── part1
├── README.md
├── anecdotes
│ ├── README.md
│ ├── package.json
│ ├── pnpm-lock.yaml
│ ├── public
│ │ ├── favicon.ico
│ │ ├── index.html
│ │ ├── logo192.png
│ │ ├── logo512.png
│ │ ├── manifest.json
│ │ └── robots.txt
│ └── src
│ │ ├── index.css
│ │ ├── index.js
│ │ └── setupTests.js
├── courseinfo
│ ├── README.md
│ ├── package.json
│ ├── pnpm-lock.yaml
│ ├── public
│ │ ├── favicon.ico
│ │ ├── index.html
│ │ ├── logo192.png
│ │ ├── logo512.png
│ │ ├── manifest.json
│ │ └── robots.txt
│ └── src
│ │ ├── index.css
│ │ ├── index.js
│ │ └── setupTests.js
└── unicafe
│ ├── README.md
│ ├── package.json
│ ├── pnpm-lock.yaml
│ ├── public
│ ├── favicon.ico
│ ├── index.html
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── robots.txt
│ └── src
│ ├── index.css
│ ├── index.js
│ └── setupTests.js
├── part2
├── README.md
├── countries
│ ├── .env.example
│ ├── README.md
│ ├── package.json
│ ├── pnpm-lock.yaml
│ ├── public
│ │ ├── favicon.ico
│ │ ├── index.html
│ │ ├── logo192.png
│ │ ├── logo512.png
│ │ ├── manifest.json
│ │ └── robots.txt
│ └── src
│ │ ├── App.js
│ │ ├── components
│ │ ├── Countries.js
│ │ ├── Country.js
│ │ ├── Filter.js
│ │ ├── Weather.js
│ │ └── WeatherInfo.js
│ │ ├── index.js
│ │ └── setupTests.js
├── course-contents
│ ├── README.md
│ ├── package.json
│ ├── pnpm-lock.yaml
│ ├── public
│ │ ├── favicon.ico
│ │ ├── index.html
│ │ ├── logo192.png
│ │ ├── logo512.png
│ │ ├── manifest.json
│ │ └── robots.txt
│ └── src
│ │ ├── components
│ │ └── Course.js
│ │ ├── index.css
│ │ ├── index.js
│ │ └── setupTests.js
└── phonebook
│ ├── README.md
│ ├── db.json
│ ├── package.json
│ ├── pnpm-lock.yaml
│ ├── public
│ ├── favicon.ico
│ ├── index.html
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── robots.txt
│ ├── routes.json
│ └── src
│ ├── App.js
│ ├── components
│ ├── Alert.js
│ ├── Filter.js
│ ├── PersonForm.js
│ └── Persons.js
│ ├── index.css
│ ├── index.js
│ ├── services
│ └── persons.js
│ └── setupTests.js
├── part4
└── bloglist-backend
│ ├── .env.example
│ ├── .eslintignore
│ ├── .eslintrc.js
│ ├── README.md
│ ├── app.js
│ ├── controllers
│ ├── blogs.js
│ ├── login.js
│ ├── testing.js
│ └── users.js
│ ├── index.js
│ ├── jest.config.js
│ ├── models
│ ├── blog.js
│ └── user.js
│ ├── package.json
│ ├── pnpm-lock.yaml
│ ├── tests
│ ├── blogs_api.int.test.js
│ ├── list_helper.test.js
│ ├── mockDb_helper.js
│ └── test_helper.js
│ └── utils
│ ├── config.js
│ ├── db_helper.js
│ ├── error_helper.js
│ ├── list_helper.js
│ ├── logger.js
│ └── middleware.js
├── part5
├── README.md
└── bloglist-frontend
│ ├── .eslintignore
│ ├── .eslintrc.js
│ ├── README.md
│ ├── cypress.json
│ ├── cypress
│ ├── fixtures
│ │ ├── blogs.json
│ │ └── users.json
│ ├── integration
│ │ └── App.spec.js
│ ├── plugins
│ │ └── index.js
│ └── support
│ │ ├── commands.js
│ │ ├── index.js
│ │ └── utils.js
│ ├── package.json
│ ├── pnpm-lock.yaml
│ ├── public
│ ├── favicon.ico
│ ├── index.html
│ └── manifest.json
│ └── src
│ ├── App.js
│ ├── App.test.js
│ ├── UserContext.js
│ ├── components
│ ├── Alert.js
│ ├── AlertList.js
│ ├── Blog.js
│ ├── Blog.test.js
│ ├── BlogForm.js
│ ├── BlogForm.test.js
│ ├── BlogList.js
│ ├── Login.js
│ ├── Modal.js
│ ├── ModalSpinner.js
│ ├── NavBar.js
│ ├── ToTopScroller.js
│ └── Toggleable.js
│ ├── fonts
│ ├── Dosis-Light.woff2
│ ├── Dosis-Medium.woff2
│ ├── Roboto-Bold-webfont.woff
│ └── Roboto-Regular-webfont.woff
│ ├── helpers
│ └── testHelper.js
│ ├── hooks
│ └── index.js
│ ├── index.css
│ ├── index.js
│ ├── services
│ ├── __mocks__
│ │ ├── blogs.js
│ │ └── login.js
│ ├── blogs.js
│ └── login.js
│ └── setupTests.js
├── part6
├── README.md
├── redux-anecdotes
│ ├── .eslintignore
│ ├── .eslintrc.js
│ ├── README.md
│ ├── db.json
│ ├── package.json
│ ├── pnpm-lock.yaml
│ ├── public
│ │ ├── favicon.ico
│ │ ├── index.html
│ │ └── manifest.json
│ └── src
│ │ ├── App.js
│ │ ├── components
│ │ ├── AnecdoteForm.js
│ │ ├── AnecdoteList.js
│ │ ├── Filter.js
│ │ ├── Modal.js
│ │ ├── ModalSpinner.js
│ │ ├── Notification.js
│ │ ├── ProgressBar.js
│ │ └── StyledComponents.js
│ │ ├── fonts
│ │ ├── FlexoW01Regular.woff
│ │ └── FlexoW01Regular.woff2
│ │ ├── helpers
│ │ └── helper.js
│ │ ├── index.js
│ │ ├── reducers
│ │ ├── anecdoteReducer.js
│ │ ├── anecdoteReducer.test.js
│ │ ├── filterReducer.js
│ │ ├── filterReducer.test.js
│ │ ├── notificationReducer.js
│ │ ├── notificationReducer.test.js
│ │ ├── requestReducer.js
│ │ └── requestReducer.test.js
│ │ ├── services
│ │ └── anecdotes.js
│ │ └── store.js
└── unicafe-redux
│ ├── .eslintignore
│ ├── .eslintrc.js
│ ├── README.md
│ ├── package.json
│ ├── pnpm-lock.yaml
│ ├── public
│ ├── favicon.ico
│ ├── index.html
│ └── manifest.json
│ └── src
│ ├── components.js
│ ├── index.js
│ ├── reducer.js
│ └── reducer.test.js
├── part7
├── README.md
├── bloglist-backend
│ ├── .env.example
│ ├── .eslintignore
│ ├── .eslintrc.js
│ ├── README.md
│ ├── app.js
│ ├── controllers
│ │ ├── blogs.js
│ │ ├── login.js
│ │ ├── testing.js
│ │ └── users.js
│ ├── index.js
│ ├── jest.config.js
│ ├── models
│ │ ├── blog.js
│ │ └── user.js
│ ├── package.json
│ ├── pnpm-lock.yaml
│ ├── requests
│ │ └── blogs_api.rest
│ ├── tests
│ │ ├── blogs_api.int.test.js
│ │ ├── list_helper.test.js
│ │ ├── mockDb_helper.js
│ │ └── test_helper.js
│ └── utils
│ │ ├── config.js
│ │ ├── db_helper.js
│ │ ├── error_helper.js
│ │ ├── list_helper.js
│ │ ├── logger.js
│ │ └── middleware.js
├── bloglist-frontend
│ ├── .eslintignore
│ ├── .eslintrc.js
│ ├── README.md
│ ├── __mocks__
│ │ ├── fileMock.js
│ │ └── styleMock.js
│ ├── cypress.json
│ ├── cypress
│ │ ├── fixtures
│ │ │ ├── blogs.json
│ │ │ └── users.json
│ │ ├── integration
│ │ │ └── App.spec.js
│ │ ├── plugins
│ │ │ └── index.js
│ │ └── support
│ │ │ ├── commands.js
│ │ │ ├── index.js
│ │ │ └── utils.js
│ ├── package.json
│ ├── pnpm-lock.yaml
│ ├── public
│ │ ├── favicon.ico
│ │ └── index.html
│ ├── src
│ │ ├── App.js
│ │ ├── App.test.js
│ │ ├── components
│ │ │ ├── Blog.js
│ │ │ ├── BlogForm.js
│ │ │ ├── BlogList.js
│ │ │ ├── Comments.js
│ │ │ ├── Home.js
│ │ │ ├── Login.js
│ │ │ ├── Modal.js
│ │ │ ├── ModalSpinner.js
│ │ │ ├── NavBar.js
│ │ │ ├── NotFound.js
│ │ │ ├── NotFoundPage.js
│ │ │ ├── Notification.js
│ │ │ ├── NotificationList.js
│ │ │ ├── PrivateRoute.js
│ │ │ ├── PublicRoute.js
│ │ │ ├── ToTopScroller.js
│ │ │ ├── Toggleable.js
│ │ │ ├── User.js
│ │ │ └── Users.js
│ │ ├── configureStore.js
│ │ ├── fonts
│ │ │ ├── Dosis-Light.woff2
│ │ │ ├── Dosis-Medium.woff2
│ │ │ ├── Roboto-Bold-webfont.woff
│ │ │ └── Roboto-Regular-webfont.woff
│ │ ├── helpers
│ │ │ └── testHelper.js
│ │ ├── hooks
│ │ │ └── index.js
│ │ ├── index.css
│ │ ├── index.js
│ │ ├── reducers
│ │ │ ├── authReducer.js
│ │ │ ├── blogReducer.js
│ │ │ ├── blogReducer.test.js
│ │ │ ├── index.js
│ │ │ ├── notificationReducer.js
│ │ │ ├── notificationReducer.test.js
│ │ │ ├── requestReducer.js
│ │ │ ├── requestReducer.test.js
│ │ │ └── userReducer.js
│ │ ├── services
│ │ │ ├── blogs.js
│ │ │ ├── login.js
│ │ │ └── users.js
│ │ ├── setupTests.js
│ │ └── utils
│ │ │ └── index.js
│ └── webpack.config.js
├── country-hook
│ ├── README.md
│ ├── package.json
│ ├── pnpm-lock.yaml
│ ├── public
│ │ ├── favicon.ico
│ │ ├── index.html
│ │ ├── logo192.png
│ │ ├── logo512.png
│ │ ├── manifest.json
│ │ └── robots.txt
│ └── src
│ │ ├── App.js
│ │ └── index.js
├── routed-anecdotes
│ ├── .eslintignore
│ ├── .eslintrc.js
│ ├── README.md
│ ├── package.json
│ ├── pnpm-lock.yaml
│ ├── public
│ │ ├── favicon.ico
│ │ ├── index.html
│ │ └── manifest.json
│ └── src
│ │ ├── App.js
│ │ ├── components
│ │ ├── About.js
│ │ ├── Anecdote.js
│ │ ├── AnecdoteList.js
│ │ ├── CreateNew.js
│ │ ├── Footer.js
│ │ └── Menu.js
│ │ ├── hooks
│ │ └── index.js
│ │ └── index.js
└── ultimate-hooks
│ ├── README.md
│ ├── db.json
│ ├── package.json
│ ├── pnpm-lock.yaml
│ ├── public
│ ├── favicon.ico
│ ├── index.html
│ └── manifest.json
│ └── src
│ ├── App.js
│ └── index.js
├── part8
├── README.md
├── library-backend
│ ├── .env.example
│ ├── .eslintrc.js
│ ├── README.md
│ ├── graphql
│ │ ├── loaders.js
│ │ ├── resolvers.js
│ │ ├── schema
│ │ │ ├── author.js
│ │ │ ├── book.js
│ │ │ ├── root.js
│ │ │ └── user.js
│ │ └── typeDefs.js
│ ├── helpers
│ │ └── errorHelper.js
│ ├── index.js
│ ├── models
│ │ ├── Author.js
│ │ ├── Book.js
│ │ └── User.js
│ ├── package.json
│ ├── pnpm-lock.yaml
│ └── utils
│ │ ├── config.js
│ │ └── logger.js
└── library-frontend
│ ├── README.md
│ ├── package.json
│ ├── pnpm-lock.yaml
│ ├── public
│ ├── favicon.ico
│ ├── index.html
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── robots.txt
│ └── src
│ ├── App.js
│ ├── components
│ ├── Authors.js
│ ├── AuthorsForm.js
│ ├── AuthorsTable.js
│ ├── Books.js
│ ├── BooksTable.js
│ ├── LinkedNavBar.js
│ ├── Login.js
│ ├── LoginForm.js
│ ├── Modal.js
│ ├── ModalSpinner.js
│ ├── NewBook.js
│ ├── NoResource.js
│ ├── Notification.js
│ ├── Notifications.js
│ ├── RecommendedBooks.js
│ └── StyledComponents.js
│ ├── graphql
│ ├── queries.js
│ └── resolvers.js
│ ├── helpers
│ └── errorHelper.js
│ ├── hooks
│ ├── useAuthUser.js
│ ├── useBookGenres.js
│ ├── useNotification.js
│ ├── useUpdateCache.js
│ └── useYupValidationResolver.js
│ ├── index.js
│ └── utils
│ ├── limitArrayOfObjectsByDate.js
│ └── logger.js
└── part9
├── README.md
├── patientor-backend
├── .eslintignore
├── .eslintrc
├── README.md
├── build
│ ├── data
│ │ ├── diagnoses.js
│ │ └── patients.js
│ └── src
│ │ ├── index.js
│ │ ├── routes
│ │ ├── diagnoses.js
│ │ └── patients.js
│ │ ├── services
│ │ ├── diagnosisService.js
│ │ └── patientService.js
│ │ ├── types.js
│ │ └── utils.js
├── data
│ ├── diagnoses.ts
│ └── patients.ts
├── package.json
├── pnpm-lock.yaml
├── src
│ ├── index.ts
│ ├── routes
│ │ ├── diagnoses.ts
│ │ └── patients.ts
│ ├── services
│ │ ├── diagnosisService.ts
│ │ └── patientService.ts
│ ├── types.ts
│ └── utils.ts
└── tsconfig.json
├── patientor-frontend
├── .eslintrc
├── README.md
├── package.json
├── pnpm-lock.yaml
├── public
│ ├── favicon.ico
│ ├── index.html
│ └── manifest.json
├── src
│ ├── AddEntryModal
│ │ ├── AddEntryForm.tsx
│ │ ├── AddEntryFormWrapper.tsx
│ │ ├── EntryTypeFields.tsx
│ │ └── index.tsx
│ ├── AddPatientModal
│ │ ├── AddPatientForm.tsx
│ │ └── index.tsx
│ ├── App.tsx
│ ├── PatientListPage
│ │ └── index.tsx
│ ├── PatientPage
│ │ ├── DiagnosisList.tsx
│ │ ├── EntryDetails.tsx
│ │ ├── HealthCheckEntry.tsx
│ │ ├── HospitalEntry.tsx
│ │ ├── OccupationalHealthCareEntry.tsx
│ │ └── index.tsx
│ ├── components
│ │ ├── FormField.tsx
│ │ └── HealthRatingBar.tsx
│ ├── constants.ts
│ ├── helpers
│ │ └── errorHelper.ts
│ ├── index.tsx
│ ├── react-app-env.d.ts
│ ├── state
│ │ ├── index.ts
│ │ ├── reducer.ts
│ │ └── state.tsx
│ ├── types.ts
│ └── utils.ts
└── tsconfig.json
├── ts-first-steps
├── .eslintrc
├── README.md
├── calculateBmi.ts
├── exerciseCalculator.ts
├── index.ts
├── package.json
├── pnpm-lock.yaml
└── tsconfig.json
└── typed-courseinfo
├── .eslintrc
├── README.md
├── package.json
├── pnpm-lock.yaml
├── public
├── favicon.ico
├── index.html
├── logo192.png
├── logo512.png
├── manifest.json
└── robots.txt
├── src
├── App.tsx
├── components
│ ├── Content.tsx
│ ├── Header.tsx
│ ├── Part.tsx
│ └── Total.tsx
├── index.css
├── index.tsx
├── react-app-env.d.ts
├── setupTests.ts
├── types.ts
└── utils.ts
└── tsconfig.json
/.gitignore:
--------------------------------------------------------------------------------
1 | # dependencies
2 | **/node_modules
3 | **/.pnp
4 | **/.pnp.js
5 |
6 | # testing
7 | **/coverage
8 | **/cypress/screenshots/**
9 | **/cypress/videos/**
10 |
11 | # production
12 | **/build
13 | # !/part7/bloglist-backend/build
14 | !/part9/patientor-backend/build
15 |
16 | # linters
17 | # **/.eslintrc.js*
18 | # **/.eslintignore
19 |
20 | # misc
21 | **/.DS_Store
22 | **/.env.local
23 | **/.env.development.local
24 | **/.env.test.local
25 | **/.env.production.local
26 | **/.env
27 |
28 | # vscode REST Client
29 | part4/bloglist-backend/requests/
30 |
31 | # vscode
32 | **/.vscode
33 | **/jsConfig.json
34 |
35 | # logs
36 | *.log
37 | **/*.log
38 | **/npm-debug.log*
39 | **/yarn-debug.log*
40 | **/yarn-error.log*
41 | **/pnpm-debug.log*
--------------------------------------------------------------------------------
/part0/0.4_new_note.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeremy-ebinum/full-stack-open-2020/ce8ebbefb5f60c71362b12849129167348891680/part0/0.4_new_note.png
--------------------------------------------------------------------------------
/part0/0.5_get_single_page_app.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeremy-ebinum/full-stack-open-2020/ce8ebbefb5f60c71362b12849129167348891680/part0/0.5_get_single_page_app.png
--------------------------------------------------------------------------------
/part0/0.6_single_page_app_new_note.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeremy-ebinum/full-stack-open-2020/ce8ebbefb5f60c71362b12849129167348891680/part0/0.6_single_page_app_new_note.png
--------------------------------------------------------------------------------
/part0/README.md:
--------------------------------------------------------------------------------
1 | # Full Stack Open 2020 - Exercise Solutions
2 |
3 | ## Part 0 - [Fundamentals of Web Apps](https://fullstackopen.com/en/part0)
4 |
5 | ## [Exercise Descriptions](https://fullstackopen.com/en/part0/fundamentals_of_web_apps#exercises-0-1-0-6)
6 |
7 | ### 0.4: new note:
8 |
9 | 
10 |
11 | ### 0.5: Single page application
12 |
13 | 
14 |
15 | ### 0.6: New note:
16 |
17 | 
18 |
--------------------------------------------------------------------------------
/part1/README.md:
--------------------------------------------------------------------------------
1 | # Full Stack Open 2020 - Exercise Solutions
2 |
3 | ## Part 1 - [Introduction to React](https://fullstackopen.com/en/part1)
4 |
5 | Each of the following folders contain the final solutions to their respective exercises
6 |
7 | ### [courseinfo](https://github.com/jeremy-ebinum/full-stack-open-2020/tree/master/part1/courseinfo)
8 |
9 | - Exercises 1.1 - 1.2 — [Description](https://fullstackopen.com/en/part1/introduction_to_react#exercises-1-1-1-2)
10 | - Exercises 1.3 - 1.5 — [Description](https://fullstackopen.com/en/part1/javascript#exercises-1-3-1-5)
11 |
12 | ### [unicafe](https://github.com/jeremy-ebinum/full-stack-open-2020/tree/master/part1/unicafe)
13 |
14 | - Exercises 1.6 - 1.11 — [Description](https://fullstackopen.com/en/part1/a_more_complex_state_debugging_react_apps#exercises-1-6-1-14)
15 |
16 | ### [anecdotes](https://github.com/jeremy-ebinum/full-stack-open-2020/tree/master/part1/anecdotes)
17 |
18 | - Exercises 1.12 - 1.14 — [Description](https://fullstackopen.com/en/part1/a_more_complex_state_debugging_react_apps#exercises-1-6-1-14)
19 |
--------------------------------------------------------------------------------
/part1/anecdotes/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "anecdotes",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^4.2.4",
7 | "@testing-library/react": "^9.4.0",
8 | "@testing-library/user-event": "^7.2.1",
9 | "react": "^16.12.0",
10 | "react-dom": "^16.12.0",
11 | "react-scripts": "3.4.0"
12 | },
13 | "scripts": {
14 | "start": "react-scripts start",
15 | "build": "react-scripts build",
16 | "test": "react-scripts test",
17 | "eject": "react-scripts eject"
18 | },
19 | "eslintConfig": {
20 | "extends": "react-app"
21 | },
22 | "browserslist": {
23 | "production": [
24 | ">0.2%",
25 | "not dead",
26 | "not op_mini all"
27 | ],
28 | "development": [
29 | "last 1 chrome version",
30 | "last 1 firefox version",
31 | "last 1 safari version"
32 | ]
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/part1/anecdotes/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeremy-ebinum/full-stack-open-2020/ce8ebbefb5f60c71362b12849129167348891680/part1/anecdotes/public/favicon.ico
--------------------------------------------------------------------------------
/part1/anecdotes/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeremy-ebinum/full-stack-open-2020/ce8ebbefb5f60c71362b12849129167348891680/part1/anecdotes/public/logo192.png
--------------------------------------------------------------------------------
/part1/anecdotes/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeremy-ebinum/full-stack-open-2020/ce8ebbefb5f60c71362b12849129167348891680/part1/anecdotes/public/logo512.png
--------------------------------------------------------------------------------
/part1/anecdotes/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/part1/anecdotes/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/part1/anecdotes/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/part1/anecdotes/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom/extend-expect';
6 |
--------------------------------------------------------------------------------
/part1/courseinfo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "courseinfo",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^4.2.4",
7 | "@testing-library/react": "^9.4.0",
8 | "@testing-library/user-event": "^7.2.1",
9 | "react": "^16.12.0",
10 | "react-dom": "^16.12.0",
11 | "react-scripts": "3.4.0"
12 | },
13 | "scripts": {
14 | "start": "react-scripts start",
15 | "build": "react-scripts build",
16 | "test": "react-scripts test",
17 | "eject": "react-scripts eject"
18 | },
19 | "eslintConfig": {
20 | "extends": "react-app"
21 | },
22 | "browserslist": {
23 | "production": [
24 | ">0.2%",
25 | "not dead",
26 | "not op_mini all"
27 | ],
28 | "development": [
29 | "last 1 chrome version",
30 | "last 1 firefox version",
31 | "last 1 safari version"
32 | ]
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/part1/courseinfo/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeremy-ebinum/full-stack-open-2020/ce8ebbefb5f60c71362b12849129167348891680/part1/courseinfo/public/favicon.ico
--------------------------------------------------------------------------------
/part1/courseinfo/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeremy-ebinum/full-stack-open-2020/ce8ebbefb5f60c71362b12849129167348891680/part1/courseinfo/public/logo192.png
--------------------------------------------------------------------------------
/part1/courseinfo/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeremy-ebinum/full-stack-open-2020/ce8ebbefb5f60c71362b12849129167348891680/part1/courseinfo/public/logo512.png
--------------------------------------------------------------------------------
/part1/courseinfo/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/part1/courseinfo/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/part1/courseinfo/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/part1/courseinfo/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom/extend-expect';
6 |
--------------------------------------------------------------------------------
/part1/unicafe/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "unicafe",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^4.2.4",
7 | "@testing-library/react": "^9.4.0",
8 | "@testing-library/user-event": "^7.2.1",
9 | "react": "^16.12.0",
10 | "react-dom": "^16.12.0",
11 | "react-scripts": "3.4.0"
12 | },
13 | "scripts": {
14 | "start": "react-scripts start",
15 | "build": "react-scripts build",
16 | "test": "react-scripts test",
17 | "eject": "react-scripts eject"
18 | },
19 | "eslintConfig": {
20 | "extends": "react-app"
21 | },
22 | "browserslist": {
23 | "production": [
24 | ">0.2%",
25 | "not dead",
26 | "not op_mini all"
27 | ],
28 | "development": [
29 | "last 1 chrome version",
30 | "last 1 firefox version",
31 | "last 1 safari version"
32 | ]
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/part1/unicafe/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeremy-ebinum/full-stack-open-2020/ce8ebbefb5f60c71362b12849129167348891680/part1/unicafe/public/favicon.ico
--------------------------------------------------------------------------------
/part1/unicafe/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeremy-ebinum/full-stack-open-2020/ce8ebbefb5f60c71362b12849129167348891680/part1/unicafe/public/logo192.png
--------------------------------------------------------------------------------
/part1/unicafe/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeremy-ebinum/full-stack-open-2020/ce8ebbefb5f60c71362b12849129167348891680/part1/unicafe/public/logo512.png
--------------------------------------------------------------------------------
/part1/unicafe/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/part1/unicafe/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/part1/unicafe/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/part1/unicafe/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom/extend-expect';
6 |
--------------------------------------------------------------------------------
/part2/README.md:
--------------------------------------------------------------------------------
1 | # Full Stack Open 2020 - Exercise Solutions
2 |
3 | ## Part 2 - [Communicating with server](https://fullstackopen.com/en/part2)
4 |
5 | Each of the following folders contain the final solutions to their respective exercises
6 |
7 | ### [course-contents](https://github.com/jeremy-ebinum/full-stack-open-2020/tree/master/part2/course-contents)
8 |
9 | - Exercises 2.1 - 2.5 — [Description](https://fullstackopen.com/en/part2/rendering_a_collection_modules#exercises-2-1-2-5)
10 |
11 | ### [countries](https://github.com/jeremy-ebinum/full-stack-open-2020/tree/master/part2/countries)
12 |
13 | - Exercises 2.12 - 2.14 — [Description](https://fullstackopen.com/en/part2/getting_data_from_server#exercises-2-11-2-14)
14 |
15 | ### [phonebook](https://github.com/jeremy-ebinum/full-stack-open-2020/tree/master/part2/phonebook)
16 |
17 | - Exercises 2.6 - 2.10 — [Description](https://fullstackopen.com/en/part2/forms#exercises-2-6-2-10)
18 | - Exercise 2.11 — [Description](https://fullstackopen.com/en/part2/getting_data_from_server#exercises-2-11-2-14)
19 | - Exercise 2.15 - 2.18 — [Description](https://fullstackopen.com/en/part2/altering_data_in_server#exercises-2-15-2-18)
20 | - Exercise 2.19 - 2.20 — [Description](https://fullstackopen.com/en/part2/adding_styles_to_react_app#exercises-2-19-2-20)
21 |
--------------------------------------------------------------------------------
/part2/countries/.env.example:
--------------------------------------------------------------------------------
1 | REACT_APP_OPENWEATHERMAP_KEY=
--------------------------------------------------------------------------------
/part2/countries/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "countries",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^4.2.4",
7 | "@testing-library/react": "^9.4.0",
8 | "@testing-library/user-event": "^7.2.1",
9 | "axios": "^0.19.2",
10 | "react": "^16.12.0",
11 | "react-dom": "^16.12.0",
12 | "react-scripts": "3.4.0"
13 | },
14 | "scripts": {
15 | "start": "react-scripts start",
16 | "build": "react-scripts build",
17 | "test": "react-scripts test",
18 | "eject": "react-scripts eject"
19 | },
20 | "eslintConfig": {
21 | "extends": "react-app"
22 | },
23 | "browserslist": {
24 | "production": [
25 | ">0.2%",
26 | "not dead",
27 | "not op_mini all"
28 | ],
29 | "development": [
30 | "last 1 chrome version",
31 | "last 1 firefox version",
32 | "last 1 safari version"
33 | ]
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/part2/countries/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeremy-ebinum/full-stack-open-2020/ce8ebbefb5f60c71362b12849129167348891680/part2/countries/public/favicon.ico
--------------------------------------------------------------------------------
/part2/countries/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeremy-ebinum/full-stack-open-2020/ce8ebbefb5f60c71362b12849129167348891680/part2/countries/public/logo192.png
--------------------------------------------------------------------------------
/part2/countries/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeremy-ebinum/full-stack-open-2020/ce8ebbefb5f60c71362b12849129167348891680/part2/countries/public/logo512.png
--------------------------------------------------------------------------------
/part2/countries/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/part2/countries/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/part2/countries/src/components/Countries.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Country from "./Country";
3 |
4 | const Countries = ({ countries, handleClick }) => {
5 | const tooManyCountries = countries.length > 10;
6 | const multipleCountries = countries.length > 1 && countries.length <= 10;
7 | const singleCountry = countries.length === 1;
8 |
9 | const countriesList = countries.map((country) => {
10 | return (
11 |
12 | {country.name}{" "}
13 |
14 | Show
15 |
16 |
17 | );
18 | });
19 |
20 | return (
21 |
22 | {tooManyCountries && "Too many matches, specify another filter"}
23 | {multipleCountries &&
{countriesList}
}
24 | {singleCountry &&
}
25 |
26 | );
27 | };
28 |
29 | export default Countries;
30 |
--------------------------------------------------------------------------------
/part2/countries/src/components/Filter.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const Filter = (props) => {
4 | return (
5 |
6 | by Name:
7 |
8 |
9 | );
10 | };
11 |
12 | export default Filter;
13 |
--------------------------------------------------------------------------------
/part2/countries/src/components/Weather.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import axios from "axios";
3 | import WeatherInfo from "./WeatherInfo";
4 |
5 | const Weather = ({ query }) => {
6 | const [condition, setCondition] = useState({});
7 | const [hasCondition, setHasCondition] = useState(false);
8 |
9 | const key = process.env.REACT_APP_OPENWEATHERMAP_KEY || null;
10 |
11 | const params = {
12 | q: query,
13 | APPID: key,
14 | };
15 |
16 | const updateCondition = () => {
17 | if (!key) return;
18 |
19 | let source = axios.CancelToken.source();
20 |
21 | axios
22 | .get("http://api.openweathermap.org/data/2.5/weather", {
23 | params: params,
24 | cancelToken: source.token,
25 | })
26 | .catch((error) => {
27 | if (axios.isCancel(error)) {
28 | console.log("Request canceled", error.message);
29 | } else {
30 | throw error;
31 | }
32 | })
33 | .then((response) => {
34 | if (response.statusText === "OK") {
35 | setCondition(response.data);
36 | setHasCondition(true);
37 | }
38 | })
39 | .catch((error) => {
40 | console.log(error.config);
41 | });
42 |
43 | return () => {
44 | source.cancel("Weather component is unmounting");
45 | };
46 | };
47 | useEffect(updateCondition, []);
48 |
49 | return {hasCondition && }
;
50 | };
51 |
52 | export default Weather;
53 |
--------------------------------------------------------------------------------
/part2/countries/src/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import App from "./App";
4 |
5 | ReactDOM.render( , document.getElementById("root"));
6 |
--------------------------------------------------------------------------------
/part2/countries/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom/extend-expect';
6 |
--------------------------------------------------------------------------------
/part2/course-contents/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "course-contents",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^4.2.4",
7 | "@testing-library/react": "^9.4.0",
8 | "@testing-library/user-event": "^7.2.1",
9 | "react": "^16.12.0",
10 | "react-dom": "^16.12.0",
11 | "react-scripts": "3.4.0"
12 | },
13 | "scripts": {
14 | "start": "react-scripts start",
15 | "build": "react-scripts build",
16 | "test": "react-scripts test",
17 | "eject": "react-scripts eject"
18 | },
19 | "eslintConfig": {
20 | "extends": "react-app"
21 | },
22 | "browserslist": {
23 | "production": [
24 | ">0.2%",
25 | "not dead",
26 | "not op_mini all"
27 | ],
28 | "development": [
29 | "last 1 chrome version",
30 | "last 1 firefox version",
31 | "last 1 safari version"
32 | ]
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/part2/course-contents/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeremy-ebinum/full-stack-open-2020/ce8ebbefb5f60c71362b12849129167348891680/part2/course-contents/public/favicon.ico
--------------------------------------------------------------------------------
/part2/course-contents/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeremy-ebinum/full-stack-open-2020/ce8ebbefb5f60c71362b12849129167348891680/part2/course-contents/public/logo192.png
--------------------------------------------------------------------------------
/part2/course-contents/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeremy-ebinum/full-stack-open-2020/ce8ebbefb5f60c71362b12849129167348891680/part2/course-contents/public/logo512.png
--------------------------------------------------------------------------------
/part2/course-contents/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/part2/course-contents/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/part2/course-contents/src/components/Course.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const Header = (props) => {
4 | return {props.name} ;
5 | };
6 |
7 | const Part = (props) => {
8 | return (
9 |
10 | {props.name} {props.exercises}
11 |
12 | );
13 | };
14 |
15 | const Content = ({ parts }) => {
16 | const partsList = parts.map((item) => {
17 | return ;
18 | });
19 |
20 | const noParts = !Array.isArray(partsList) || !partsList.length;
21 |
22 | return (
23 |
24 | {noParts &&
This course doesn't have any parts yet.
}
25 | {!noParts && partsList}
26 |
27 | );
28 | };
29 |
30 | const Total = ({ parts }) => {
31 | const total = parts.reduce((acc, item) => acc + item.exercises, 0);
32 |
33 | return (
34 |
35 | Total of {total} exercises
36 |
37 | );
38 | };
39 |
40 | const Course = ({ course }) => {
41 | return (
42 |
43 |
44 |
45 |
46 |
47 | );
48 | };
49 |
50 | export default Course;
51 |
--------------------------------------------------------------------------------
/part2/course-contents/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/part2/course-contents/src/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import "./index.css";
4 | import Course from "./components/Course";
5 |
6 | const App = () => {
7 | const courses = [
8 | {
9 | name: "Half Stack application development",
10 | id: 1,
11 | parts: [
12 | {
13 | name: "Fundamentals of React",
14 | exercises: 10,
15 | id: 1,
16 | },
17 | {
18 | name: "Using props to pass data",
19 | exercises: 7,
20 | id: 2,
21 | },
22 | {
23 | name: "State of a component",
24 | exercises: 14,
25 | id: 3,
26 | },
27 | {
28 | name: "Redux",
29 | exercises: 11,
30 | id: 4,
31 | },
32 | ],
33 | },
34 | {
35 | name: "Node.js",
36 | id: 2,
37 | parts: [
38 | {
39 | name: "Routing",
40 | exercises: 3,
41 | id: 1,
42 | },
43 | {
44 | name: "Middlewares",
45 | exercises: 7,
46 | id: 2,
47 | },
48 | ],
49 | },
50 | ];
51 |
52 | const courseList = courses.map((course) => {
53 | return ;
54 | });
55 |
56 | return {courseList}
;
57 | };
58 |
59 | ReactDOM.render( , document.getElementById("root"));
60 |
--------------------------------------------------------------------------------
/part2/course-contents/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom/extend-expect';
6 |
--------------------------------------------------------------------------------
/part2/phonebook/db.json:
--------------------------------------------------------------------------------
1 | {
2 | "persons": [
3 | {
4 | "name": "Arto Hellas",
5 | "number": "040-123456",
6 | "id": 1
7 | },
8 | {
9 | "name": "Ada Lovelace",
10 | "number": "39-44-5323523",
11 | "id": 2
12 | },
13 | {
14 | "name": "Dan Abramov",
15 | "number": "12-43-234345",
16 | "id": 3
17 | },
18 | {
19 | "name": "Mary Poppendieck",
20 | "number": "39-23-6423122",
21 | "id": 4
22 | }
23 | ]
24 | }
--------------------------------------------------------------------------------
/part2/phonebook/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "phonebook",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^4.2.4",
7 | "@testing-library/react": "^9.4.0",
8 | "@testing-library/user-event": "^7.2.1",
9 | "axios": "^0.19.2",
10 | "react": "^16.12.0",
11 | "react-dom": "^16.12.0",
12 | "react-scripts": "3.4.0",
13 | "unique-random": "^2.1.0"
14 | },
15 | "scripts": {
16 | "start": "react-scripts start",
17 | "build": "react-scripts build",
18 | "test": "react-scripts test",
19 | "eject": "react-scripts eject",
20 | "server": "json-server -p3001 db.json --routes routes.json"
21 | },
22 | "proxy": "http://localhost:3001",
23 | "eslintConfig": {
24 | "extends": "react-app"
25 | },
26 | "browserslist": {
27 | "production": [
28 | ">0.2%",
29 | "not dead",
30 | "not op_mini all"
31 | ],
32 | "development": [
33 | "last 1 chrome version",
34 | "last 1 firefox version",
35 | "last 1 safari version"
36 | ]
37 | },
38 | "devDependencies": {
39 | "json-server": "^0.15.1"
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/part2/phonebook/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeremy-ebinum/full-stack-open-2020/ce8ebbefb5f60c71362b12849129167348891680/part2/phonebook/public/favicon.ico
--------------------------------------------------------------------------------
/part2/phonebook/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeremy-ebinum/full-stack-open-2020/ce8ebbefb5f60c71362b12849129167348891680/part2/phonebook/public/logo192.png
--------------------------------------------------------------------------------
/part2/phonebook/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeremy-ebinum/full-stack-open-2020/ce8ebbefb5f60c71362b12849129167348891680/part2/phonebook/public/logo512.png
--------------------------------------------------------------------------------
/part2/phonebook/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/part2/phonebook/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/part2/phonebook/routes.json:
--------------------------------------------------------------------------------
1 | {
2 | "/api/*": "/$1",
3 | "/:resource/": "/:resource/",
4 | "/:resource/:id/": "/:resource/:id"
5 | }
6 |
--------------------------------------------------------------------------------
/part2/phonebook/src/components/Alert.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 |
3 | // run the timeoutFunc after a given period with useEffect hook
4 | const Alert = ({ timeoutFunc, id, type, message }) => {
5 | const alertTypes = {
6 | error: "c-alert--error",
7 | success: "c-alert--success",
8 | info: "c-alert--info",
9 | };
10 |
11 | if (!alertTypes[type]) throw new Error("Invalid Alert Type");
12 |
13 | const removeAlert = () => timeoutFunc(id);
14 |
15 | useEffect(() => {
16 | setTimeout(removeAlert, 3000);
17 | });
18 |
19 | return (
20 |
21 | {message}
22 |
23 | );
24 | };
25 |
26 | export default Alert;
27 |
--------------------------------------------------------------------------------
/part2/phonebook/src/components/Filter.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const Filter = (props) => {
4 | return (
5 |
6 |
Search Contacts
7 |
8 |
15 |
16 |
17 | );
18 | };
19 |
20 | export default Filter;
21 |
--------------------------------------------------------------------------------
/part2/phonebook/src/components/PersonForm.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const PersonForm = (props) => {
4 | return (
5 |
37 | );
38 | };
39 |
40 | export default PersonForm;
41 |
--------------------------------------------------------------------------------
/part2/phonebook/src/components/Persons.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const Person = (props) => {
4 | return (
5 |
6 |
7 | {props.name}:
8 |
9 |
10 | {props.number},
11 |
12 |
13 |
14 |
19 | Delete
20 |
21 |
22 |
23 |
24 | );
25 | };
26 |
27 | const Persons = ({ persons, handleClick }) => {
28 | const personsList = persons.map((person) => {
29 | return (
30 |
37 | );
38 | });
39 |
40 | return (
41 |
42 |
Contact List
43 | {personsList}
44 |
45 | );
46 | };
47 |
48 | export default Persons;
49 |
--------------------------------------------------------------------------------
/part2/phonebook/src/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import App from "./App";
4 | import "./index.css";
5 |
6 | ReactDOM.render( , document.getElementById("root"));
7 |
--------------------------------------------------------------------------------
/part2/phonebook/src/services/persons.js:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 |
3 | const baseUrl = "/api/persons";
4 |
5 | const create = (newPerson) => {
6 | const request = axios.post(baseUrl, newPerson);
7 |
8 | return request.then((response) => response.data);
9 | };
10 |
11 | const getAll = () => {
12 | const request = axios.get(baseUrl);
13 |
14 | return request.then((response) => response.data);
15 | };
16 |
17 | const update = (id, newPerson) => {
18 | const request = axios.put(`${baseUrl}/${id}`, newPerson);
19 |
20 | return request.then((response) => response.data);
21 | };
22 |
23 | const remove = (id) => {
24 | const request = axios.delete(`${baseUrl}/${id}`);
25 |
26 | return request;
27 | };
28 |
29 | export default { create, getAll, update, remove };
30 |
--------------------------------------------------------------------------------
/part2/phonebook/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom/extend-expect';
6 |
--------------------------------------------------------------------------------
/part4/bloglist-backend/.env.example:
--------------------------------------------------------------------------------
1 | PORT=3001
2 | MONGODB_URI=mongodb://[username:password@]host1[:port1][,...hostN[:portN]][/[database][?options]]
3 | SECRET_KEY=n-e(hc^3d4i!5h$#78_2!zjaw42bqq5n+uze_7peyv6dcbly4a
--------------------------------------------------------------------------------
/part4/bloglist-backend/.eslintignore:
--------------------------------------------------------------------------------
1 | build
--------------------------------------------------------------------------------
/part4/bloglist-backend/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | commonjs: true,
4 | es6: true,
5 | node: true,
6 | jest: true,
7 | },
8 | extends: ["airbnb-base", "plugin:prettier/recommended"],
9 | globals: {
10 | Atomics: "readonly",
11 | SharedArrayBuffer: "readonly",
12 | },
13 | parserOptions: {
14 | ecmaVersion: 2018,
15 | },
16 | plugins: ["prettier"],
17 | rules: {
18 | "prettier/prettier": [
19 | "error",
20 | {
21 | trailingComma: "es5",
22 | arrowParens: "always",
23 | printWidth: 80,
24 | tabWidth: 2,
25 | semi: true,
26 | singleQuote: false,
27 | bracketSpacing: true,
28 | },
29 | ],
30 | "no-console": 0,
31 | "no-param-reassign": ["error", { props: false }],
32 | "no-underscore-dangle": ["error", { allow: ["_id", "__v"] }],
33 | },
34 | };
35 |
--------------------------------------------------------------------------------
/part4/bloglist-backend/app.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const bodyParser = require("body-parser");
3 | const cors = require("cors");
4 | const logger = require("./utils/logger");
5 | const db = require("./utils/db_helper");
6 | const mockDb = require("./tests/mockDb_helper");
7 | const middleware = require("./utils/middleware");
8 | const usersRouter = require("./controllers/users");
9 | const loginRouter = require("./controllers/login");
10 | const blogsRouter = require("./controllers/blogs");
11 | const testingRouter = require("./controllers/testing");
12 |
13 | const app = express();
14 |
15 | if (process.env.NODE_ENV === "test") {
16 | mockDb.connect().catch((err) => {
17 | logger.error(err);
18 | });
19 | app.use("/api/testing", testingRouter);
20 | } else {
21 | db.connect().catch((err) => {
22 | logger.error(err);
23 | });
24 | }
25 |
26 | app.use(cors());
27 | app.use(bodyParser.json());
28 | app.use(middleware.morganLogger());
29 | app.use(middleware.tokenExtractor);
30 |
31 | app.use("/api/users", usersRouter);
32 | app.use("/api/login", loginRouter);
33 | app.use("/api/blogs", blogsRouter);
34 | app.use(middleware.unknownRouteHandler);
35 |
36 | app.use(middleware.errorHandler);
37 |
38 | module.exports = app;
39 |
--------------------------------------------------------------------------------
/part4/bloglist-backend/controllers/login.js:
--------------------------------------------------------------------------------
1 | const bcrypt = require("bcrypt");
2 | const jwt = require("jsonwebtoken");
3 | const loginRouter = require("express").Router();
4 | const { ErrorHelper } = require("../utils/error_helper");
5 | const User = require("../models/user");
6 |
7 | loginRouter.post("/", async (req, res, next) => {
8 | const { body } = req;
9 |
10 | try {
11 | const user = await User.findOne({ username: body.username });
12 | const isCorrectPassword =
13 | user === null
14 | ? false
15 | : await bcrypt.compare(body.password, user.passwordHash);
16 |
17 | if (!(user && isCorrectPassword)) {
18 | throw new ErrorHelper(401, "Authentication Error", [
19 | "Invalid username or password",
20 | ]);
21 | }
22 |
23 | const userForToken = {
24 | username: user.username,
25 | id: user._id,
26 | };
27 |
28 | const token = jwt.sign(userForToken, process.env.SECRET_KEY);
29 |
30 | res.status(200).send({ token, username: user.username, name: user.name });
31 | } catch (err) {
32 | next(err);
33 | }
34 | });
35 |
36 | module.exports = loginRouter;
37 |
--------------------------------------------------------------------------------
/part4/bloglist-backend/controllers/testing.js:
--------------------------------------------------------------------------------
1 | const router = require("express").Router();
2 | const mockDb = require("../tests/mockDb_helper");
3 | const testHelper = require("../tests/test_helper");
4 |
5 | router.post("/reset", async (request, response) => {
6 | mockDb.clearDatabase();
7 | response.status(204).end();
8 | });
9 |
10 | router.get("/blogs", async (req, res) => {
11 | const blogsInDb = await testHelper.getBlogsInDb();
12 |
13 | res.status(200).json(blogsInDb);
14 | });
15 |
16 | router.get("/users", async (req, res) => {
17 | const usersInDb = await testHelper.getUsersInDb();
18 |
19 | res.status(200).json(usersInDb);
20 | });
21 |
22 | module.exports = router;
23 |
--------------------------------------------------------------------------------
/part4/bloglist-backend/index.js:
--------------------------------------------------------------------------------
1 | const http = require("http");
2 | const config = require("./utils/config");
3 | const app = require("./app");
4 | const logger = require("./utils/logger");
5 |
6 | const server = http.createServer(app);
7 |
8 | server.listen(config.PORT, () => {
9 | logger.info(`Server running on port ${config.PORT}`);
10 | });
11 |
--------------------------------------------------------------------------------
/part4/bloglist-backend/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | testEnvironment: "node",
3 | };
4 |
--------------------------------------------------------------------------------
/part4/bloglist-backend/models/blog.js:
--------------------------------------------------------------------------------
1 | const mongoose = require("mongoose");
2 |
3 | const blogSchema = mongoose.Schema({
4 | title: { type: String, required: true },
5 | author: String,
6 | url: { type: String, required: true },
7 | likes: Number,
8 | user: { type: mongoose.SchemaTypes.ObjectId, ref: "User" },
9 | });
10 |
11 | blogSchema.set("toJSON", {
12 | transform: (document, returnedObject) => {
13 | returnedObject.id = returnedObject._id.toString();
14 | delete returnedObject._id;
15 | delete returnedObject.__v;
16 | },
17 | });
18 |
19 | module.exports = mongoose.model("Blog", blogSchema);
20 |
--------------------------------------------------------------------------------
/part4/bloglist-backend/models/user.js:
--------------------------------------------------------------------------------
1 | const mongoose = require("mongoose");
2 | const uniqueValidator = require("mongoose-unique-validator");
3 |
4 | const userSchema = mongoose.Schema({
5 | username: { type: String, unique: true },
6 | passwordHash: { type: String },
7 | name: { type: String },
8 | blogs: [{ type: mongoose.SchemaTypes.ObjectId, ref: "Blog" }],
9 | });
10 |
11 | userSchema.plugin(uniqueValidator);
12 |
13 | userSchema.set("toJSON", {
14 | transform: (document, returnedObject) => {
15 | returnedObject.id = returnedObject._id.toString();
16 | delete returnedObject._id;
17 | delete returnedObject.passwordHash;
18 | delete returnedObject.__v;
19 | },
20 | });
21 |
22 | module.exports = mongoose.model("User", userSchema);
23 |
--------------------------------------------------------------------------------
/part4/bloglist-backend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "bloglist-backend",
3 | "version": "1.0.0",
4 | "main": "index.js",
5 | "scripts": {
6 | "start": "cross-env NODE_ENV=production node index.js",
7 | "start:test": "cross-env NODE_ENV=test node index.js",
8 | "watch": "cross-env NODE_ENV=development nodemon index.js",
9 | "lint": "eslint .",
10 | "test": "cross-env NODE_ENV=test jest --verbose --runInBand"
11 | },
12 | "keywords": [],
13 | "author": "Jeremy Ebinum",
14 | "license": "MIT",
15 | "description": "",
16 | "dependencies": {
17 | "bcrypt": "^4.0.1",
18 | "body-parser": "^1.19.0",
19 | "cors": "^2.8.5",
20 | "dotenv": "^8.2.0",
21 | "express": "^4.17.1",
22 | "jsonwebtoken": "^8.5.1",
23 | "lodash": "^4.17.15",
24 | "mongoose": "^5.8.11",
25 | "mongoose-unique-validator": "^2.0.3",
26 | "morgan": "^1.9.1"
27 | },
28 | "devDependencies": {
29 | "cross-env": "^7.0.0",
30 | "eslint": "^6.1.0",
31 | "eslint-config-airbnb-base": "^14.0.0",
32 | "eslint-config-prettier": "^6.10.0",
33 | "eslint-plugin-import": "^2.18.2",
34 | "eslint-plugin-prettier": "^3.1.2",
35 | "jest": "^25.1.0",
36 | "mongodb-memory-server-global": "^6.2.4",
37 | "nodemon": "^2.0.2",
38 | "prettier": "^1.19.1",
39 | "supertest": "^4.0.2"
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/part4/bloglist-backend/tests/mockDb_helper.js:
--------------------------------------------------------------------------------
1 | const mongoose = require("mongoose");
2 | const { MongoMemoryServer } = require("mongodb-memory-server-global");
3 |
4 | const mongod = new MongoMemoryServer();
5 |
6 | /**
7 | * Connect to the in-memory database.
8 | */
9 | module.exports.connect = async () => {
10 | const uri = await mongod.getConnectionString();
11 |
12 | const mongooseOpts = {
13 | useNewUrlParser: true,
14 | useUnifiedTopology: true,
15 | };
16 |
17 | mongoose.set("useCreateIndex", true);
18 | mongoose.set("useFindAndModify", false);
19 |
20 | await mongoose.connect(uri, mongooseOpts);
21 | };
22 |
23 | /**
24 | * Drop database, close the connection and stop mongod.
25 | */
26 | module.exports.closeDatabase = async () => {
27 | await mongoose.connection.dropDatabase();
28 | await mongoose.connection.close();
29 | await mongod.stop();
30 | };
31 |
32 | /**
33 | * Remove all the data for all db collections.
34 | */
35 | module.exports.clearDatabase = async () => {
36 | const { collections } = mongoose.connection;
37 |
38 | Object.keys(collections).forEach(async (key) => {
39 | const collection = collections[key];
40 | await collection.deleteMany();
41 | });
42 | };
43 |
--------------------------------------------------------------------------------
/part4/bloglist-backend/utils/config.js:
--------------------------------------------------------------------------------
1 | require("dotenv").config();
2 |
3 | const { PORT, MONGODB_URI } = process.env;
4 |
5 | module.exports = { PORT, MONGODB_URI };
6 |
--------------------------------------------------------------------------------
/part4/bloglist-backend/utils/db_helper.js:
--------------------------------------------------------------------------------
1 | const mongoose = require("mongoose");
2 | const config = require("./config");
3 | const logger = require("./logger");
4 |
5 | module.exports.connect = async () => {
6 | mongoose.connection.on("connected", () => {
7 | logger.info("Connection Established");
8 | });
9 |
10 | mongoose.connection.on("reconnected", () => {
11 | logger.info("Connection Reestablished");
12 | });
13 |
14 | mongoose.connection.on("disconnected", () => {
15 | logger.info("Connection Disconnected");
16 | });
17 |
18 | mongoose.connection.on("close", () => {
19 | logger.info("Connection Closed");
20 | });
21 |
22 | mongoose.connection.on("error", (error) => {
23 | logger.error(`ERROR: ${error}`);
24 | });
25 |
26 | if (process.env.NODE_ENV === "test") {
27 | return Promise.resolve("");
28 | }
29 |
30 | mongoose.set("useCreateIndex", true);
31 | mongoose.set("useFindAndModify", false);
32 |
33 | return mongoose.connect(config.MONGODB_URI, {
34 | useNewUrlParser: true,
35 | useUnifiedTopology: true,
36 | });
37 | };
38 |
--------------------------------------------------------------------------------
/part4/bloglist-backend/utils/error_helper.js:
--------------------------------------------------------------------------------
1 | const logger = require("../utils/logger");
2 |
3 | class ErrorHelper extends Error {
4 | constructor(statusCode, kind, messages = []) {
5 | super();
6 | this.statusCode = statusCode;
7 | this.kind = kind;
8 | this.messages = messages;
9 | this.message = this.messages.join("\n");
10 | }
11 | }
12 | const handleError = (err, res) => {
13 | try {
14 | const { statusCode, kind, message, messages } = err;
15 |
16 | res.status(statusCode).json({
17 | statusCode,
18 | kind,
19 | message,
20 | messages,
21 | });
22 | } catch (exception) {
23 | logger.error("IN handleError helper\n", exception.message);
24 | }
25 | };
26 |
27 | module.exports = {
28 | ErrorHelper,
29 | handleError,
30 | };
31 |
--------------------------------------------------------------------------------
/part4/bloglist-backend/utils/logger.js:
--------------------------------------------------------------------------------
1 | const info = (...params) => {
2 | if (process.env.NODE_ENV !== "test") {
3 | console.log(...params);
4 | }
5 | };
6 |
7 | const error = (...params) => {
8 | console.error(...params);
9 | };
10 |
11 | module.exports = {
12 | info,
13 | error,
14 | };
15 |
--------------------------------------------------------------------------------
/part5/README.md:
--------------------------------------------------------------------------------
1 | # Full Stack Open 2020 - Exercise Solutions
2 |
3 | ## Part 5 - [Testing React apps, custom hooks](https://fullstackopen.com/en/part5)
4 |
5 | Each of the following folders contain the final solutions to their respective exercises
6 |
7 | ### [bloglist-frontend](https://github.com/jeremy-ebinum/full-stack-open-2020/tree/master/part5/bloglist-frontend)
8 |
9 | - Exercises 5.1 - 5.4 — [Description](https://fullstackopen.com/en/part5/login_in_frontend#exercises-5-1-5-4)
10 | - Exercises 5.5 - 5.10 — [Description](https://fullstackopen.com/en/part5/props_children_and_proptypes#exercises-5-5-5-10)
11 | - Exercises 5.11 - 5.12 — [Description](https://fullstackopen.com/en/part5/props_children_and_proptypes#exercises-5-11-5-12)
12 | - Exercises 5.17 - 5.22 — [Description](https://fullstackopen.com/en/part5/end_to_end_testing#exercises-5-17-5-22)
13 |
--------------------------------------------------------------------------------
/part5/bloglist-frontend/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | build
--------------------------------------------------------------------------------
/part5/bloglist-frontend/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | es6: true,
4 | node: true,
5 | "jest/globals": true,
6 | "cypress/globals": true,
7 | },
8 | extends: ["react-app", "plugin:prettier/recommended"],
9 | globals: {
10 | Atomics: "readonly",
11 | SharedArrayBuffer: "readonly",
12 | },
13 | parserOptions: {
14 | ecmaVersion: 2018,
15 | },
16 | plugins: ["jest", "cypress", "prettier"],
17 | rules: {
18 | "prettier/prettier": [
19 | "error",
20 | {
21 | trailingComma: "es5",
22 | arrowParens: "always",
23 | printWidth: 80,
24 | tabWidth: 2,
25 | semi: true,
26 | singleQuote: false,
27 | bracketSpacing: true,
28 | },
29 | ],
30 | "react/jsx-filename-extension": [1, { extensions: [".js", ".jsx"] }],
31 | "no-param-reassign": ["error", { props: false }],
32 | "no-console": 0,
33 | // "react/prop-types": 0,
34 | "jsx-a11y/label-has-associated-control": [
35 | 2,
36 | {
37 | labelComponents: [],
38 | labelAttributes: [],
39 | controlComponents: ["Input"],
40 | assert: "either",
41 | depth: 3,
42 | },
43 | ],
44 | },
45 | };
46 |
--------------------------------------------------------------------------------
/part5/bloglist-frontend/cypress.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/part5/bloglist-frontend/cypress/fixtures/blogs.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "title": "First Test Blog Title",
4 | "author": "First Test Blog Author",
5 | "url": "http://site.com"
6 | },
7 | {
8 | "title": "Second Test Blog Title",
9 | "author": "Second Test Blog Author",
10 | "url": "http://site.com"
11 | },
12 | {
13 | "title": "Third Test Blog Title",
14 | "author": "Third Test Blog Author",
15 | "url": "http://site.com"
16 | }
17 | ]
18 |
--------------------------------------------------------------------------------
/part5/bloglist-frontend/cypress/fixtures/users.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "name": "Test User",
4 | "username": "test_username",
5 | "password": "test_password"
6 | },
7 | {
8 | "name": "Other Test User",
9 | "username": "other_test_username",
10 | "password": "ohter_test_password"
11 | }
12 | ]
13 |
--------------------------------------------------------------------------------
/part5/bloglist-frontend/cypress/plugins/index.js:
--------------------------------------------------------------------------------
1 | ///
2 | // ***********************************************************
3 | // This example plugins/index.js can be used to load plugins
4 | //
5 | // You can change the location of this file or turn off loading
6 | // the plugins file with the 'pluginsFile' configuration option.
7 | //
8 | // You can read more here:
9 | // https://on.cypress.io/plugins-guide
10 | // ***********************************************************
11 |
12 | // This function is called when a project is opened or re-opened (e.g. due to
13 | // the project's config changing)
14 |
15 | /**
16 | * @type {Cypress.PluginConfig}
17 | */
18 | module.exports = (on, config) => {
19 | // `on` is used to hook into various events Cypress emits
20 | // `config` is the resolved Cypress config
21 | };
22 |
--------------------------------------------------------------------------------
/part5/bloglist-frontend/cypress/support/commands.js:
--------------------------------------------------------------------------------
1 | // ***********************************************
2 | // This example commands.js shows you how to
3 | // create various custom commands and overwrite
4 | // existing commands.
5 | //
6 | // For more comprehensive examples of custom
7 | // commands please read more here:
8 | // https://on.cypress.io/custom-commands
9 | // ***********************************************
10 | //
11 | //
12 | // -- This is a parent command --
13 | // Cypress.Commands.add("login", (email, password) => { ... })
14 | //
15 | //
16 | // -- This is a child command --
17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
18 | //
19 | //
20 | // -- This is a dual command --
21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
22 | //
23 | //
24 | // -- This will overwrite an existing command --
25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
26 |
--------------------------------------------------------------------------------
/part5/bloglist-frontend/cypress/support/index.js:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example support/index.js is processed and
3 | // loaded automatically before your test files.
4 | //
5 | // This is a great place to put global configuration and
6 | // behavior that modifies Cypress.
7 | //
8 | // You can change the location of this file or turn off
9 | // automatically serving support files with the
10 | // 'supportFile' configuration option.
11 | //
12 | // You can read more here:
13 | // https://on.cypress.io/configuration
14 | // ***********************************************************
15 |
16 | // Import commands.js using ES2015 syntax:
17 | import "./commands";
18 |
19 | // Alternatively you can use CommonJS syntax:
20 | // require('./commands')
21 |
--------------------------------------------------------------------------------
/part5/bloglist-frontend/cypress/support/utils.js:
--------------------------------------------------------------------------------
1 | export const login = (username, password) => {
2 | cy.get("[data-testid='Login_username']").type(username);
3 | cy.get("[data-testid='Login_password']").type(password);
4 | cy.get("[data-testid='Login_submitButton']").click();
5 | };
6 |
7 | export const createBlog = ({ title, author, url }) => {
8 | cy.get("[data-testid='BlogForm_title']").type(title);
9 | cy.get("[data-testid='BlogForm_author']").type(author);
10 | cy.get("[data-testid='BlogForm_url']").type(url);
11 | cy.get("[data-testid='BlogForm_submitButton']").click();
12 | };
13 |
--------------------------------------------------------------------------------
/part5/bloglist-frontend/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeremy-ebinum/full-stack-open-2020/ce8ebbefb5f60c71362b12849129167348891680/part5/bloglist-frontend/public/favicon.ico
--------------------------------------------------------------------------------
/part5/bloglist-frontend/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/part5/bloglist-frontend/src/UserContext.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const UserContext = React.createContext(null);
4 |
5 | export default UserContext;
6 |
--------------------------------------------------------------------------------
/part5/bloglist-frontend/src/components/Alert.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import PropTypes from "prop-types";
3 | import { getTestIDs } from "../helpers/testHelper";
4 |
5 | export const testIDs = getTestIDs();
6 |
7 | // run the timeoutFunc after a given period with useEffect hook
8 | const Alert = ({ timeoutFunc, id, type, contextClass, message }) => {
9 | const alertTypes = {
10 | error: "c-alert--error",
11 | success: "c-alert--success",
12 | info: "c-alert--info",
13 | };
14 |
15 | if (!alertTypes[type]) throw new Error("Invalid Alert Type");
16 |
17 | useEffect(() => {
18 | const removeAlert = () => timeoutFunc(id);
19 | setTimeout(removeAlert, 3000);
20 | }, [id, timeoutFunc]);
21 |
22 | return (
23 |
27 | {message}
28 |
29 | );
30 | };
31 |
32 | Alert.propTypes = {
33 | timeoutFunc: PropTypes.func.isRequired,
34 | id: PropTypes.string.isRequired,
35 | type: PropTypes.string.isRequired,
36 | contextClass: PropTypes.string.isRequired,
37 | message: PropTypes.string.isRequired,
38 | };
39 |
40 | export default React.memo(Alert);
41 |
--------------------------------------------------------------------------------
/part5/bloglist-frontend/src/components/AlertList.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import Alert from "./Alert";
4 |
5 | const AlertList = ({ alerts, contextClass }) => {
6 | const alertsToDisplay = alerts.map((alert) => {
7 | return (
8 |
16 | );
17 | });
18 |
19 | return alertsToDisplay;
20 | };
21 |
22 | AlertList.propTypes = {
23 | alerts: PropTypes.arrayOf(PropTypes.object).isRequired,
24 | contextClass: PropTypes.string.isRequired,
25 | };
26 |
27 | const shouldNotUpdate = (prevProps, nextProps) => {
28 | const sameAlerts = prevProps.alerts.length === nextProps.alerts.length;
29 | const sameContextClass = prevProps.contextClass === nextProps.contextClass;
30 |
31 | if (sameAlerts && sameContextClass) {
32 | return true;
33 | }
34 |
35 | return false;
36 | };
37 |
38 | export default React.memo(AlertList, shouldNotUpdate);
39 |
--------------------------------------------------------------------------------
/part5/bloglist-frontend/src/components/BlogForm.test.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { render, fireEvent, cleanup } from "@testing-library/react";
3 | import testHelper from "../helpers/testHelper";
4 | import BlogForm from "./BlogForm";
5 |
6 | describe(" ", () => {
7 | let createBlog;
8 |
9 | beforeEach(() => {
10 | createBlog = jest.fn().mockName("handleDelete");
11 | });
12 |
13 | afterEach(() => {
14 | cleanup();
15 | });
16 |
17 | test("correctly calls the handler prop on submit", () => {
18 | const { getByLabelText, getByRole } = render(
19 |
20 | );
21 |
22 | const form = getByRole("form");
23 | const titleInput = getByLabelText(/title/i);
24 | const authorInput = getByLabelText(/author/i);
25 | const urlInput = getByLabelText(/url/i);
26 |
27 | fireEvent.change(titleInput, {
28 | target: { value: testHelper.validNewBlog.title },
29 | });
30 | fireEvent.change(authorInput, {
31 | target: { value: testHelper.validNewBlog.author },
32 | });
33 | fireEvent.change(urlInput, {
34 | target: { value: testHelper.validNewBlog.url },
35 | });
36 |
37 | fireEvent.submit(form);
38 |
39 | expect(createBlog).toHaveBeenCalledWith(testHelper.validNewBlog);
40 | });
41 | });
42 |
--------------------------------------------------------------------------------
/part5/bloglist-frontend/src/components/BlogList.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import Blog from "./Blog";
4 | import { getTestIDs } from "../helpers/testHelper";
5 |
6 | export const testIDs = getTestIDs();
7 |
8 | const BlogList = ({ fetchHasRun, blogs, handleLike, handleDelete }) => {
9 | return (
10 |
11 |
Blogs
12 |
13 | {fetchHasRun && !blogs.length && (
14 |
No Blogs have been added yet
15 | )}
16 |
17 | {blogs.map((blog) => {
18 | return (
19 |
25 | );
26 | })}
27 |
28 | );
29 | };
30 |
31 | BlogList.propTypes = {
32 | blogs: PropTypes.arrayOf(PropTypes.object).isRequired,
33 | fetchHasRun: PropTypes.bool.isRequired,
34 | handleLike: PropTypes.func.isRequired,
35 | handleDelete: PropTypes.func.isRequired,
36 | };
37 |
38 | export default React.memo(BlogList);
39 |
--------------------------------------------------------------------------------
/part5/bloglist-frontend/src/components/Modal.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import PropTypes from "prop-types";
3 |
4 | const Modal = ({ testid, children }) => {
5 | useEffect(() => {
6 | const rootStyle = document.documentElement.style;
7 | const wrapper = document.querySelector(".js-wrapper");
8 | rootStyle.setProperty("--body-overflow", "hidden");
9 | wrapper.classList.add("o-wrapper--hasModal");
10 | return () => {
11 | rootStyle.setProperty("--body-overflow", "auto");
12 | wrapper.classList.remove("o-wrapper--hasModal");
13 | };
14 | }, []);
15 |
16 | return (
17 |
20 | );
21 | };
22 |
23 | Modal.propTypes = {
24 | children: PropTypes.node.isRequired,
25 | testid: PropTypes.string,
26 | };
27 |
28 | Modal.defaultProps = {
29 | testid: null,
30 | };
31 |
32 | export default Modal;
33 |
--------------------------------------------------------------------------------
/part5/bloglist-frontend/src/components/ModalSpinner.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
3 | import { faSpinner } from "@fortawesome/free-solid-svg-icons";
4 | import { getTestIDs } from "../helpers/testHelper";
5 | import Modal from "./Modal";
6 |
7 | export const testIDs = getTestIDs();
8 |
9 | const ModalSpinner = () => {
10 | return (
11 |
12 |
17 |
18 | );
19 | };
20 |
21 | export default ModalSpinner;
22 |
--------------------------------------------------------------------------------
/part5/bloglist-frontend/src/components/ToTopScroller.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useImperativeHandle } from "react";
2 |
3 | const ToTopScroller = React.forwardRef((props, ref) => {
4 | const [isHidden, setIsHidden] = useState(true);
5 |
6 | const style = { display: isHidden ? "none" : "" };
7 |
8 | const scrollToTop = () => {
9 | document.documentElement.scrollTop = 0;
10 | };
11 |
12 | const show = () => {
13 | setIsHidden(false);
14 | };
15 |
16 | const hide = () => {
17 | setIsHidden(true);
18 | };
19 |
20 | useImperativeHandle(ref, () => {
21 | return {
22 | show,
23 | hide,
24 | };
25 | });
26 |
27 | return (
28 |
29 |
34 | Back To Top
35 |
36 |
37 | );
38 | });
39 |
40 | export default React.memo(ToTopScroller);
41 |
--------------------------------------------------------------------------------
/part5/bloglist-frontend/src/fonts/Dosis-Light.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeremy-ebinum/full-stack-open-2020/ce8ebbefb5f60c71362b12849129167348891680/part5/bloglist-frontend/src/fonts/Dosis-Light.woff2
--------------------------------------------------------------------------------
/part5/bloglist-frontend/src/fonts/Dosis-Medium.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeremy-ebinum/full-stack-open-2020/ce8ebbefb5f60c71362b12849129167348891680/part5/bloglist-frontend/src/fonts/Dosis-Medium.woff2
--------------------------------------------------------------------------------
/part5/bloglist-frontend/src/fonts/Roboto-Bold-webfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeremy-ebinum/full-stack-open-2020/ce8ebbefb5f60c71362b12849129167348891680/part5/bloglist-frontend/src/fonts/Roboto-Bold-webfont.woff
--------------------------------------------------------------------------------
/part5/bloglist-frontend/src/fonts/Roboto-Regular-webfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeremy-ebinum/full-stack-open-2020/ce8ebbefb5f60c71362b12849129167348891680/part5/bloglist-frontend/src/fonts/Roboto-Regular-webfont.woff
--------------------------------------------------------------------------------
/part5/bloglist-frontend/src/hooks/index.js:
--------------------------------------------------------------------------------
1 | import { useState, useCallback } from "react";
2 |
3 | export const useField = ({
4 | id = null,
5 | className = "c-row__input",
6 | type = "text",
7 | name = null,
8 | placeholder = null,
9 | }) => {
10 | const [value, setValue] = useState("");
11 |
12 | const onChange = useCallback((event) => {
13 | setValue(event.target.value);
14 | }, []);
15 |
16 | const reset = useCallback(() => setValue(""), []);
17 |
18 | return [{ id, className, type, name, placeholder, value, onChange }, reset];
19 | };
20 |
--------------------------------------------------------------------------------
/part5/bloglist-frontend/src/index.js:
--------------------------------------------------------------------------------
1 | /* istanbul ignore file */
2 | import React from "react";
3 | import ReactDOM from "react-dom";
4 | import App from "./App";
5 | import "./index.css";
6 |
7 | if (process.env.NODE_ENV === "development") {
8 | const whyDidYouRender = require("@welldone-software/why-did-you-render");
9 | whyDidYouRender(React);
10 | }
11 |
12 | ReactDOM.render( , document.getElementById("root"));
13 |
--------------------------------------------------------------------------------
/part5/bloglist-frontend/src/services/__mocks__/blogs.js:
--------------------------------------------------------------------------------
1 | import testHelper from "../../helpers/testHelper";
2 |
3 | const blogs = testHelper.blogs;
4 | // eslint-disable-next-line no-unused-vars
5 | let token = null;
6 |
7 | const setToken = (newToken) => {
8 | if (newToken) {
9 | token = `Bearer ${newToken}`;
10 | } else {
11 | token = null;
12 | }
13 | };
14 |
15 | const getAll = () => {
16 | return Promise.resolve(blogs);
17 | };
18 |
19 | export default { setToken, getAll };
20 |
--------------------------------------------------------------------------------
/part5/bloglist-frontend/src/services/__mocks__/login.js:
--------------------------------------------------------------------------------
1 | import testHelper from "../../helpers/testHelper";
2 |
3 | const returnedUser = testHelper.validLoggedInUser;
4 |
5 | const login = async () => {
6 | return Promise.resolve(returnedUser);
7 | };
8 |
9 | export default { login };
10 |
--------------------------------------------------------------------------------
/part5/bloglist-frontend/src/services/blogs.js:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 | import testHelper from "../helpers/testHelper";
3 |
4 | if (process.env.NODE_ENV === "test") {
5 | axios.defaults.adapter = require("axios/lib/adapters/http");
6 | axios.defaults.baseURL = testHelper.host;
7 | }
8 |
9 | const baseUrl = "/api/blogs";
10 |
11 | let token = null;
12 |
13 | const setToken = (newToken) => {
14 | if (newToken) {
15 | token = `Bearer ${newToken}`;
16 | } else {
17 | token = null;
18 | }
19 | };
20 |
21 | const create = async (newBlog) => {
22 | const config = { headers: { Authorization: token } };
23 | const response = await axios.post(baseUrl, newBlog, config);
24 |
25 | return response.data;
26 | };
27 |
28 | const getAll = async () => {
29 | const response = await axios.get(baseUrl);
30 |
31 | return response.data;
32 | };
33 |
34 | const update = async (id, updatedBlog) => {
35 | const config = { headers: { Authorization: token } };
36 | const response = await axios.put(`${baseUrl}/${id}`, updatedBlog, config);
37 |
38 | return response.data;
39 | };
40 |
41 | const remove = async (id) => {
42 | const config = { headers: { Authorization: token } };
43 | const response = await axios.delete(`${baseUrl}/${id}`, config);
44 |
45 | return response.data;
46 | };
47 |
48 | export default { setToken, create, getAll, update, remove };
49 |
--------------------------------------------------------------------------------
/part5/bloglist-frontend/src/services/login.js:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 | import testHelper from "../helpers/testHelper";
3 |
4 | if (process.env.NODE_ENV === "test") {
5 | axios.defaults.adapter = require("axios/lib/adapters/http");
6 | axios.defaults.baseURL = testHelper.host;
7 | }
8 |
9 | const baseUrl = "/api/login";
10 |
11 | const login = async (credentials) => {
12 | const response = await axios.post(baseUrl, credentials);
13 |
14 | return response.data;
15 | };
16 |
17 | export default { login };
18 |
--------------------------------------------------------------------------------
/part5/bloglist-frontend/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import "@testing-library/jest-dom/extend-expect";
6 |
7 | // Mocks
8 | // jest.mock("./services/blogs");
9 | // jest.mock("./services/login");
10 |
11 | // localStorage
12 | let savedItems = {};
13 |
14 | const localStorageMock = {
15 | setItem: (key, item) => {
16 | savedItems[key] = item;
17 | },
18 | getItem: (key) => savedItems[key],
19 | removeItem: (key) => delete savedItems[key],
20 | clear: () => {
21 | savedItems = {};
22 | },
23 | };
24 |
25 | global.localStorage = localStorageMock;
26 |
27 | // window.confirm
28 | window.confirm = () => true;
29 |
--------------------------------------------------------------------------------
/part6/README.md:
--------------------------------------------------------------------------------
1 | # Full Stack Open 2020 - Exercise Solutions
2 |
3 | ## Part 6 - [State management with Redux](https://fullstackopen.com/en/part6)
4 |
5 | Each of the following folders contain the final solutions to their respective exercises
6 |
7 | ### [unicafe-redux](https://github.com/jeremy-ebinum/full-stack-open-2020/tree/master/part6/unicafe-redux)
8 |
9 | - Exercises 6.1 - 6.2 — [Description](https://fullstackopen.com/en/part6/flux_architecture_and_redux#exercises-6-1-6-2)
10 |
11 | ### [redux-anecdotes](https://github.com/jeremy-ebinum/full-stack-open-2020/tree/master/part6/redux-anecdotes)
12 |
13 | - Exercises 6.3 - 6.8 — [Description](https://fullstackopen.com/en/part6/flux_architecture_and_redux#exercises-6-3-6-8)
14 | - Exercises 6.9 - 6.12 — [Description](https://fullstackopen.com/en/part6/many_reducers#exercises-6-9-6-12)
15 | - Exercises 6.13 - 6.14 — [Description](https://fullstackopen.com/en/part6/communicating_with_server_in_a_redux_application#exercises-6-13-6-14)
16 | - Exercises 6.15 - 6.18 — [Description](https://fullstackopen.com/en/part6/communicating_with_server_in_a_redux_application#exercises-6-15-6-18)
17 | - Exercises 6.19 - 6.21 — [Description](https://fullstackopen.com/en/part6/connect#exercises-6-19-6-21)
18 |
--------------------------------------------------------------------------------
/part6/redux-anecdotes/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | build
--------------------------------------------------------------------------------
/part6/redux-anecdotes/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | es6: true,
4 | node: true,
5 | "jest/globals": true,
6 | },
7 | extends: ["react-app", "plugin:prettier/recommended"],
8 | globals: {
9 | Atomics: "readonly",
10 | SharedArrayBuffer: "readonly",
11 | },
12 | parserOptions: {
13 | ecmaVersion: 2018,
14 | },
15 | plugins: ["jest", "prettier"],
16 | rules: {
17 | "prettier/prettier": [
18 | "error",
19 | {
20 | trailingComma: "es5",
21 | arrowParens: "always",
22 | printWidth: 80,
23 | tabWidth: 2,
24 | semi: true,
25 | singleQuote: false,
26 | bracketSpacing: true,
27 | },
28 | ],
29 | "react/jsx-filename-extension": [1, { extensions: [".js", ".jsx"] }],
30 | "no-param-reassign": ["error", { props: false }],
31 | "no-console": 0,
32 | // "react/prop-types": 0,
33 | "jsx-a11y/label-has-associated-control": [
34 | 2,
35 | {
36 | labelComponents: [],
37 | labelAttributes: [],
38 | controlComponents: ["Input"],
39 | assert: "either",
40 | depth: 3,
41 | },
42 | ],
43 | },
44 | };
45 |
--------------------------------------------------------------------------------
/part6/redux-anecdotes/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "redux-anecdotes",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "axios": "^0.19.2",
7 | "json-server": "^0.16.1",
8 | "prop-types": "^15.7.2",
9 | "react": "^16.12.0",
10 | "react-dom": "^16.12.0",
11 | "react-redux": "^7.2.0",
12 | "react-scripts": "3.4.0",
13 | "react-uid": "^2.2.0",
14 | "redux": "^4.0.5",
15 | "redux-devtools-extension": "^2.13.8",
16 | "redux-thunk": "^2.3.0",
17 | "styled-components": "^5.0.1",
18 | "styled-icons": "^9.5.0"
19 | },
20 | "devDependencies": {
21 | "@types/jest": "^25.1.3",
22 | "deep-freeze": "^0.0.1",
23 | "eslint-config-prettier": "^6.10.0",
24 | "eslint-plugin-jest": "^23.7.0",
25 | "eslint-plugin-prettier": "^3.1.2",
26 | "prettier": "^1.19.1"
27 | },
28 | "scripts": {
29 | "start": "react-scripts start",
30 | "build": "react-scripts build",
31 | "server": "json-server -p3001 db.json",
32 | "test": "set CI=true && react-scripts test",
33 | "test:watch": "react-scripts test",
34 | "test:coverage": "set CI=true && react-scripts test --coverage",
35 | "eject": "react-scripts eject"
36 | },
37 | "browserslist": {
38 | "production": [
39 | ">0.2%",
40 | "not dead",
41 | "not op_mini all"
42 | ],
43 | "development": [
44 | "last 1 chrome version",
45 | "last 1 firefox version",
46 | "last 1 safari version"
47 | ]
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/part6/redux-anecdotes/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeremy-ebinum/full-stack-open-2020/ce8ebbefb5f60c71362b12849129167348891680/part6/redux-anecdotes/public/favicon.ico
--------------------------------------------------------------------------------
/part6/redux-anecdotes/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/part6/redux-anecdotes/src/components/Filter.js:
--------------------------------------------------------------------------------
1 | import React, { useRef } from "react";
2 | import PropTypes from "prop-types";
3 | import { connect } from "react-redux";
4 | import { useUIDSeed } from "react-uid";
5 | import { setFilter, clearFilter } from "../reducers/filterReducer";
6 | import {
7 | FilterInput,
8 | FilterLabel,
9 | FilterContainer,
10 | ClearFliterButton,
11 | } from "./StyledComponents";
12 |
13 | const Filter = ({ setFilter, clearFilter }) => {
14 | const seed = useUIDSeed();
15 | const filterInputRef = useRef();
16 |
17 | const handleChange = (event) => {
18 | setFilter(event.target.value);
19 | };
20 |
21 | const clear = () => {
22 | clearFilter();
23 | filterInputRef.current.value = "";
24 | };
25 |
26 | return (
27 |
28 | Search
29 |
35 | Clear Filter
36 |
37 | );
38 | };
39 |
40 | Filter.propTypes = {
41 | setFilter: PropTypes.func.isRequired,
42 | clearFilter: PropTypes.func.isRequired,
43 | };
44 |
45 | export default connect(null, { setFilter, clearFilter })(Filter);
46 |
--------------------------------------------------------------------------------
/part6/redux-anecdotes/src/components/Modal.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import { Modal as StyledModal, ModalContent } from "./StyledComponents";
3 |
4 | const Modal = ({ children }) => {
5 | useEffect(() => {
6 | document.body.style.overflow = "hidden";
7 | return () => {
8 | document.body.style.overflow = "";
9 | };
10 | }, []);
11 |
12 | return (
13 |
14 | {children}
15 |
16 | );
17 | };
18 |
19 | export default Modal;
20 |
--------------------------------------------------------------------------------
/part6/redux-anecdotes/src/components/ModalSpinner.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Modal from "./Modal";
3 | import { FetchSpinner } from "./StyledComponents";
4 |
5 | const ModalSpinner = () => {
6 | return (
7 |
8 |
9 |
10 | );
11 | };
12 |
13 | export default ModalSpinner;
14 |
--------------------------------------------------------------------------------
/part6/redux-anecdotes/src/components/Notification.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import { connect } from "react-redux";
4 | import { NotificationContainer, Alert } from "./StyledComponents";
5 |
6 | const Notification = ({ notificationsToShow }) => {
7 | return (
8 |
9 | {notificationsToShow.map((notification) => (
10 |
11 | {notification.message}
12 |
13 | ))}
14 |
15 | );
16 | };
17 |
18 | const notificationsToShow = (notifications) => {
19 | if (notifications.length <= 3) {
20 | return notifications;
21 | } else {
22 | const notificationsByDateDesc = [...notifications].sort(
23 | (a, b) => b.date - a.date
24 | );
25 |
26 | const lastThreeNotifications = notificationsByDateDesc
27 | .slice(0, 3)
28 | .reverse();
29 |
30 | return lastThreeNotifications;
31 | }
32 | };
33 |
34 | const mapStateToProps = (state) => {
35 | return { notificationsToShow: notificationsToShow(state.notifications) };
36 | };
37 |
38 | Notification.propTypes = {
39 | notificationsToShow: PropTypes.arrayOf(PropTypes.object).isRequired,
40 | };
41 |
42 | export default connect(mapStateToProps)(Notification);
43 |
--------------------------------------------------------------------------------
/part6/redux-anecdotes/src/components/ProgressBar.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import { connect } from "react-redux";
4 | import { Meter, MeterBar } from "./StyledComponents";
5 |
6 | const ProgressBar = ({ isLoading }) => {
7 | return (
8 | <>
9 | {isLoading && (
10 |
11 |
12 |
13 | )}
14 | >
15 | );
16 | };
17 |
18 | const mapStateToProps = (state) => {
19 | const loadingStates = Object.entries(state.requests).map((entry) => {
20 | return { ...entry[1], requestName: entry[0] };
21 | });
22 |
23 | const isLoading = loadingStates.some((l) => {
24 | return l.requestName !== "initAnecdotes" && l.isLoading;
25 | });
26 |
27 | return { isLoading };
28 | };
29 |
30 | ProgressBar.propTypes = { isLoading: PropTypes.bool.isRequired };
31 |
32 | export default connect(mapStateToProps)(ProgressBar);
33 |
--------------------------------------------------------------------------------
/part6/redux-anecdotes/src/fonts/FlexoW01Regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeremy-ebinum/full-stack-open-2020/ce8ebbefb5f60c71362b12849129167348891680/part6/redux-anecdotes/src/fonts/FlexoW01Regular.woff
--------------------------------------------------------------------------------
/part6/redux-anecdotes/src/fonts/FlexoW01Regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeremy-ebinum/full-stack-open-2020/ce8ebbefb5f60c71362b12849129167348891680/part6/redux-anecdotes/src/fonts/FlexoW01Regular.woff2
--------------------------------------------------------------------------------
/part6/redux-anecdotes/src/helpers/helper.js:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 |
3 | export const getTrimmedStr = (str) => {
4 | return str.length > 50 ? str.slice(0, 49) + "..." : str;
5 | };
6 |
7 | export const getCancelTokenSource = () => {
8 | const CancelToken = axios.CancelToken;
9 | const source = CancelToken.source();
10 |
11 | return source;
12 | };
13 |
--------------------------------------------------------------------------------
/part6/redux-anecdotes/src/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import { Provider } from "react-redux";
4 | import App from "./App";
5 | import store from "./store";
6 |
7 | ReactDOM.render(
8 |
9 |
10 | ,
11 | document.getElementById("root")
12 | );
13 |
--------------------------------------------------------------------------------
/part6/redux-anecdotes/src/reducers/filterReducer.js:
--------------------------------------------------------------------------------
1 | export const initialState = "";
2 |
3 | const filterReducer = (state = initialState, action) => {
4 | switch (action.type) {
5 | case "SET_FILTER":
6 | return action.filter;
7 | case "CLEAR_FILTER":
8 | return initialState;
9 | default:
10 | return state;
11 | }
12 | };
13 |
14 | export const setFilter = (filter) => {
15 | return (dispatch) => {
16 | dispatch({ type: "SET_FILTER", filter });
17 | };
18 | };
19 |
20 | export const clearFilter = () => {
21 | return (dispatch) => {
22 | dispatch({ type: "CLEAR_FILTER" });
23 | };
24 | };
25 |
26 | export default filterReducer;
27 |
--------------------------------------------------------------------------------
/part6/redux-anecdotes/src/reducers/filterReducer.test.js:
--------------------------------------------------------------------------------
1 | import deepFreeze from "deep-freeze";
2 | import filterReducer, { initialState } from "./filterReducer";
3 |
4 | describe("filterReducer", () => {
5 | test("returns it's initial state when initialized with an undefined state", () => {
6 | const action = {
7 | type: "DO_NOTHING",
8 | };
9 |
10 | const newState = filterReducer(undefined, action);
11 | expect(newState).toEqual(initialState);
12 | });
13 |
14 | test("SET_FILTER updates the state of filter", () => {
15 | const newFilter = "test filter";
16 |
17 | const action = {
18 | type: "SET_FILTER",
19 | filter: newFilter,
20 | };
21 |
22 | const state = initialState;
23 | deepFreeze(state);
24 |
25 | const newState = filterReducer(state, action);
26 |
27 | expect(newState).toBe(newFilter);
28 | });
29 |
30 | test("CLEAR_FILTER resets filter to initial state", () => {
31 | const state = "existing filter";
32 |
33 | const action = {
34 | type: "CLEAR_FILTER",
35 | };
36 |
37 | deepFreeze(state);
38 |
39 | const newState = filterReducer(state, action);
40 |
41 | expect(newState).toBe(initialState);
42 | });
43 | });
44 |
--------------------------------------------------------------------------------
/part6/redux-anecdotes/src/reducers/notificationReducer.js:
--------------------------------------------------------------------------------
1 | import { uid } from "react-uid";
2 |
3 | export const initialState = [];
4 |
5 | const notificationReducer = (state = initialState, action) => {
6 | switch (action.type) {
7 | case "NEW_NOTIFICATION":
8 | return state.concat(action.data);
9 | case "REMOVE_NOTIFICATION":
10 | const id = action.id;
11 | return state.filter((notification) => notification.id !== id);
12 | default:
13 | return state;
14 | }
15 | };
16 |
17 | export const newNotification = (message, level) => {
18 | return {
19 | type: "NEW_NOTIFICATION",
20 | data: { id: uid({}), message, level, date: Date.now() },
21 | };
22 | };
23 |
24 | export const removeNotification = (id) => {
25 | return {
26 | type: "REMOVE_NOTIFICATION",
27 | id,
28 | };
29 | };
30 |
31 | /**
32 | * Queues a new notification to be removed after the specified timeout
33 | * @param {string} message
34 | * @param {number} timeout
35 | * @param {string=} level - success | info | warning
36 | *
37 | * @return {function} thunk
38 | */
39 | export const displayNotification = (message, timeout, level = "") => {
40 | return (dispatch) => {
41 | const action = newNotification(message, level);
42 | const id = action.data.id;
43 |
44 | dispatch(action);
45 |
46 | setTimeout(() => {
47 | dispatch(removeNotification(id));
48 | }, timeout);
49 | };
50 | };
51 |
52 | export default notificationReducer;
53 |
--------------------------------------------------------------------------------
/part6/redux-anecdotes/src/reducers/requestReducer.test.js:
--------------------------------------------------------------------------------
1 | import deepFreeze from "deep-freeze";
2 | import requestReducer, { initialState, newStates } from "./requestReducer";
3 |
4 | describe("requestReducer()", () => {
5 | test("returns it's initial state when initialized with an undefined state", () => {
6 | const action = { type: null };
7 |
8 | const newState = requestReducer(undefined, action);
9 | expect(newState).toEqual(initialState);
10 | });
11 |
12 | test("LOADING sets the state for pending requests", () => {
13 | const action = {
14 | type: "LOADING",
15 | request: "initAnecdotes",
16 | };
17 |
18 | const state = initialState;
19 | deepFreeze(state);
20 |
21 | const newState = requestReducer(state, action);
22 | expect(newState.initAnecdotes).toEqual({
23 | ...initialState.initAnecdotes,
24 | ...newStates.loading,
25 | });
26 | });
27 |
28 | test("SUCCESS sets the state for successful requests", () => {
29 | const action = {
30 | type: "LOADING",
31 | request: "initAnecdotes",
32 | };
33 |
34 | const state = initialState;
35 | deepFreeze(state);
36 |
37 | const newState = requestReducer(state, action);
38 | expect(newState.initAnecdotes).toEqual({
39 | ...initialState.initAnecdotes,
40 | ...newStates.loading,
41 | });
42 | });
43 | });
44 |
--------------------------------------------------------------------------------
/part6/redux-anecdotes/src/services/anecdotes.js:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 |
3 | const baseUrl = "http://localhost:3001/anecdotes";
4 |
5 | const create = async (content) => {
6 | const newAnecdote = { content, votes: 0 };
7 | const response = await axios.post(baseUrl, newAnecdote);
8 | return response.data;
9 | };
10 |
11 | const getAll = async () => {
12 | const response = await axios.get(baseUrl);
13 | return response.data;
14 | };
15 |
16 | const update = async (id, updatedAnecdote) => {
17 | const response = await axios.put(`${baseUrl}/${id}`, updatedAnecdote);
18 | return response.data;
19 | };
20 |
21 | export default { create, getAll, update };
22 |
--------------------------------------------------------------------------------
/part6/redux-anecdotes/src/store.js:
--------------------------------------------------------------------------------
1 | import { createStore, combineReducers, applyMiddleware } from "redux";
2 | import { composeWithDevTools } from "redux-devtools-extension";
3 | import thunk from "redux-thunk";
4 |
5 | import anecdoteReducer from "./reducers/anecdoteReducer";
6 | import notificationReducer from "./reducers/notificationReducer";
7 | import filterReducer from "./reducers/filterReducer";
8 | import requestReducer from "./reducers/requestReducer";
9 |
10 | const reducer = combineReducers({
11 | anecdotes: anecdoteReducer,
12 | notifications: notificationReducer,
13 | filter: filterReducer,
14 | requests: requestReducer,
15 | });
16 |
17 | const store = createStore(reducer, composeWithDevTools(applyMiddleware(thunk)));
18 |
19 | export default store;
20 |
--------------------------------------------------------------------------------
/part6/unicafe-redux/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | build
--------------------------------------------------------------------------------
/part6/unicafe-redux/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | es6: true,
4 | node: true,
5 | "jest/globals": true,
6 | },
7 | extends: ["react-app", "plugin:prettier/recommended"],
8 | globals: {
9 | Atomics: "readonly",
10 | SharedArrayBuffer: "readonly",
11 | },
12 | parserOptions: {
13 | ecmaVersion: 2018,
14 | },
15 | plugins: ["jest", "prettier"],
16 | rules: {
17 | "prettier/prettier": [
18 | "error",
19 | {
20 | trailingComma: "es5",
21 | arrowParens: "always",
22 | printWidth: 80,
23 | tabWidth: 2,
24 | semi: true,
25 | singleQuote: false,
26 | bracketSpacing: true,
27 | },
28 | ],
29 | "react/jsx-filename-extension": [1, { extensions: [".js", ".jsx"] }],
30 | "no-param-reassign": ["error", { props: false }],
31 | "no-console": 0,
32 | // "react/prop-types": 0,
33 | "jsx-a11y/label-has-associated-control": [
34 | 2,
35 | {
36 | labelComponents: [],
37 | labelAttributes: [],
38 | controlComponents: ["Input"],
39 | assert: "either",
40 | depth: 3,
41 | },
42 | ],
43 | },
44 | };
45 |
--------------------------------------------------------------------------------
/part6/unicafe-redux/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "unicafe-redux",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^4.2.4",
7 | "@testing-library/react": "^9.4.0",
8 | "@testing-library/user-event": "^7.2.1",
9 | "react": "^16.12.0",
10 | "react-dom": "^16.12.0",
11 | "react-scripts": "3.4.0",
12 | "redux": "^4.0.5",
13 | "styled-components": "^5.0.1"
14 | },
15 | "scripts": {
16 | "start": "react-scripts start",
17 | "build": "react-scripts build",
18 | "test": "set CI=true && react-scripts test",
19 | "test:watch": "react-scripts test",
20 | "test:coverage": "set CI=true && react-scripts test --coverage",
21 | "eject": "react-scripts eject"
22 | },
23 | "eslintConfig": {
24 | "extends": "react-app"
25 | },
26 | "browserslist": {
27 | "production": [
28 | ">0.2%",
29 | "not dead",
30 | "not op_mini all"
31 | ],
32 | "development": [
33 | "last 1 chrome version",
34 | "last 1 firefox version",
35 | "last 1 safari version"
36 | ]
37 | },
38 | "devDependencies": {
39 | "@types/jest": "^25.1.3",
40 | "deep-freeze": "^0.0.1",
41 | "eslint-config-prettier": "^6.10.0",
42 | "eslint-plugin-jest": "^23.7.0",
43 | "eslint-plugin-prettier": "^3.1.2",
44 | "prettier": "^1.19.1"
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/part6/unicafe-redux/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeremy-ebinum/full-stack-open-2020/ce8ebbefb5f60c71362b12849129167348891680/part6/unicafe-redux/public/favicon.ico
--------------------------------------------------------------------------------
/part6/unicafe-redux/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/part6/unicafe-redux/src/reducer.js:
--------------------------------------------------------------------------------
1 | const initialState = {
2 | good: 0,
3 | ok: 0,
4 | bad: 0,
5 | };
6 |
7 | const counterReducer = (state = initialState, action) => {
8 | switch (action.type) {
9 | case "GOOD":
10 | return { ...state, good: state.good + 1 };
11 | case "OK":
12 | return { ...state, ok: state.ok + 1 };
13 | case "BAD":
14 | return { ...state, bad: state.bad + 1 };
15 | case "ZERO":
16 | return initialState;
17 | default:
18 | return state;
19 | }
20 | };
21 |
22 | export default counterReducer;
23 |
--------------------------------------------------------------------------------
/part7/bloglist-backend/.env.example:
--------------------------------------------------------------------------------
1 | PORT=3001
2 | MONGODB_URI=mongodb://[username:password@]host1[:port1][,...hostN[:portN]][/[database][?options]]
3 | SECRET_KEY=n-e(hc^3d4i!5h$#78_2!zjaw42bqq5n+uze_7peyv6dcbly4a
--------------------------------------------------------------------------------
/part7/bloglist-backend/.eslintignore:
--------------------------------------------------------------------------------
1 | build
--------------------------------------------------------------------------------
/part7/bloglist-backend/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | commonjs: true,
4 | es6: true,
5 | node: true,
6 | jest: true,
7 | },
8 | extends: ["airbnb-base", "plugin:prettier/recommended"],
9 | globals: {
10 | Atomics: "readonly",
11 | SharedArrayBuffer: "readonly",
12 | },
13 | parserOptions: {
14 | ecmaVersion: 2018,
15 | },
16 | plugins: ["prettier"],
17 | rules: {
18 | "prettier/prettier": [
19 | "error",
20 | {
21 | trailingComma: "es5",
22 | arrowParens: "always",
23 | printWidth: 80,
24 | tabWidth: 2,
25 | semi: true,
26 | singleQuote: false,
27 | bracketSpacing: true,
28 | },
29 | ],
30 | "no-console": 0,
31 | "no-param-reassign": ["error", { props: false }],
32 | "no-underscore-dangle": ["error", { allow: ["_id", "__v"] }],
33 | },
34 | };
35 |
--------------------------------------------------------------------------------
/part7/bloglist-backend/controllers/login.js:
--------------------------------------------------------------------------------
1 | const bcrypt = require("bcrypt");
2 | const jwt = require("jsonwebtoken");
3 | const loginRouter = require("express").Router();
4 | const { ErrorHelper } = require("../utils/error_helper");
5 | const User = require("../models/user");
6 |
7 | loginRouter.post("/", async (req, res, next) => {
8 | const { body } = req;
9 |
10 | try {
11 | const user = await User.findOne({ username: body.username });
12 | const isCorrectPassword =
13 | user === null
14 | ? false
15 | : await bcrypt.compare(body.password, user.passwordHash);
16 |
17 | if (!(user && isCorrectPassword)) {
18 | throw new ErrorHelper(401, "Authentication Error", [
19 | "Invalid username or password",
20 | ]);
21 | }
22 |
23 | const userForToken = {
24 | username: user.username,
25 | id: user._id,
26 | };
27 |
28 | const token = jwt.sign(userForToken, process.env.SECRET_KEY);
29 |
30 | res.status(200).send({ token, username: user.username, name: user.name });
31 | } catch (err) {
32 | next(err);
33 | }
34 | });
35 |
36 | module.exports = loginRouter;
37 |
--------------------------------------------------------------------------------
/part7/bloglist-backend/controllers/testing.js:
--------------------------------------------------------------------------------
1 | const router = require("express").Router();
2 | const mockDb = require("../tests/mockDb_helper");
3 | const testHelper = require("../tests/test_helper");
4 |
5 | router.post("/reset", async (request, response) => {
6 | mockDb.clearDatabase();
7 | response.status(204).end();
8 | });
9 |
10 | router.get("/blogs", async (req, res) => {
11 | const blogsInDb = await testHelper.getBlogsInDb();
12 |
13 | res.status(200).json(blogsInDb);
14 | });
15 |
16 | router.get("/users", async (req, res) => {
17 | const usersInDb = await testHelper.getUsersInDb();
18 |
19 | res.status(200).json(usersInDb);
20 | });
21 |
22 | module.exports = router;
23 |
--------------------------------------------------------------------------------
/part7/bloglist-backend/index.js:
--------------------------------------------------------------------------------
1 | const http = require("http");
2 | const config = require("./utils/config");
3 | const app = require("./app");
4 | const logger = require("./utils/logger");
5 |
6 | const server = http.createServer(app);
7 |
8 | server.listen(config.PORT, () => {
9 | logger.info(`Server running on port ${config.PORT}`);
10 | });
11 |
--------------------------------------------------------------------------------
/part7/bloglist-backend/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | testEnvironment: "node",
3 | };
4 |
--------------------------------------------------------------------------------
/part7/bloglist-backend/models/blog.js:
--------------------------------------------------------------------------------
1 | const mongoose = require("mongoose");
2 |
3 | const blogSchema = mongoose.Schema({
4 | title: { type: String, required: true },
5 | author: String,
6 | url: { type: String, required: true },
7 | likes: Number,
8 | user: { type: mongoose.SchemaTypes.ObjectId, ref: "User" },
9 | comments: [String],
10 | });
11 |
12 | blogSchema.set("toJSON", {
13 | transform: (document, returnedObject) => {
14 | returnedObject.id = returnedObject._id.toString();
15 | delete returnedObject._id;
16 | delete returnedObject.__v;
17 | },
18 | });
19 |
20 | module.exports = mongoose.model("Blog", blogSchema);
21 |
--------------------------------------------------------------------------------
/part7/bloglist-backend/models/user.js:
--------------------------------------------------------------------------------
1 | const mongoose = require("mongoose");
2 | const uniqueValidator = require("mongoose-unique-validator");
3 |
4 | const userSchema = mongoose.Schema({
5 | username: { type: String, unique: true },
6 | passwordHash: { type: String },
7 | name: { type: String },
8 | blogs: [{ type: mongoose.SchemaTypes.ObjectId, ref: "Blog" }],
9 | });
10 |
11 | userSchema.plugin(uniqueValidator);
12 |
13 | userSchema.set("toJSON", {
14 | transform: (document, returnedObject) => {
15 | returnedObject.id = returnedObject._id.toString();
16 | delete returnedObject._id;
17 | delete returnedObject.passwordHash;
18 | delete returnedObject.__v;
19 | },
20 | });
21 |
22 | module.exports = mongoose.model("User", userSchema);
23 |
--------------------------------------------------------------------------------
/part7/bloglist-backend/requests/blogs_api.rest:
--------------------------------------------------------------------------------
1 | # VARIABLES
2 | @baseUrl = http://localhost:3003
3 | @contentType = application/json;charset=utf-8
4 | @BlogId = {{getBlogs.response.body.$[-1:].id}}
5 | @token = {{loginUser.response.body.token}}
6 |
7 |
8 | ### Get Users Collection
9 | # @name getUsers
10 | GET {{baseUrl}}/api/users HTTP/1.1
11 |
12 |
13 | ### Create Valid User
14 | # @name createUser
15 | POST {{baseUrl}}/api/users HTTP/1.1
16 | Content-Type: {{contentType}}
17 |
18 | {
19 | "username": "username",
20 | "name": "Uchiha Madara",
21 | "password": "password"
22 | }
23 |
24 | ### Login Valid User
25 | # @name loginUser
26 | POST {{baseUrl}}/api/login HTTP/1.1
27 | Content-Type: {{contentType}}
28 |
29 | {
30 | "username": "username",
31 | "password": "password"
32 | }
33 |
34 | ### Get Blogs Collection:
35 | # @name getBlogs
36 | GET {{baseUrl}}/api/blogs HTTP/1.1
37 |
38 | ### Get Specific Blog
39 | GET {{baseUrl}}/api/blogs/{{BlogId}} HTTP/1.1
40 |
41 | ### Delete Specific Blog
42 | DELETE {{baseUrl}}/api/blogs/{{BlogId}} HTTP/1.1
43 | Authorization: Bearer {{token}}
44 |
45 | ### Create Valid Blog
46 | # @name createBlog
47 | POST {{baseUrl}}/api/blogs HTTP/1.1
48 | Content-Type: {{contentType}}
49 | Authorization: Bearer {{token}}
50 |
51 | {
52 | "title": "Type wars",
53 | "author": "Robert C. Martin",
54 | "url": "http://blog.cleancoder.com/uncle-bob/2016/05/01/TypeWars.html",
55 | "likes": 2
56 | }
57 |
--------------------------------------------------------------------------------
/part7/bloglist-backend/tests/mockDb_helper.js:
--------------------------------------------------------------------------------
1 | const mongoose = require("mongoose");
2 | const { MongoMemoryServer } = require("mongodb-memory-server-core");
3 |
4 | const mongod = new MongoMemoryServer();
5 |
6 | /**
7 | * Connect to the in-memory database.
8 | */
9 | module.exports.connect = async () => {
10 | const uri = await mongod.getConnectionString();
11 |
12 | const mongooseOpts = {
13 | useNewUrlParser: true,
14 | useUnifiedTopology: true,
15 | };
16 |
17 | mongoose.set("useCreateIndex", true);
18 | mongoose.set("useFindAndModify", false);
19 |
20 | await mongoose.connect(uri, mongooseOpts);
21 | };
22 |
23 | /**
24 | * Drop database, close the connection and stop mongod.
25 | */
26 | module.exports.closeDatabase = async () => {
27 | await mongoose.connection.dropDatabase();
28 | await mongoose.connection.close();
29 | await mongod.stop();
30 | };
31 |
32 | /**
33 | * Remove all the data for all db collections.
34 | */
35 | module.exports.clearDatabase = async () => {
36 | const { collections } = mongoose.connection;
37 |
38 | Object.keys(collections).forEach(async (key) => {
39 | const collection = collections[key];
40 | await collection.deleteMany();
41 | });
42 | };
43 |
--------------------------------------------------------------------------------
/part7/bloglist-backend/utils/config.js:
--------------------------------------------------------------------------------
1 | require("dotenv").config();
2 |
3 | const { PORT, MONGODB_URI } = process.env;
4 |
5 | module.exports = { PORT, MONGODB_URI };
6 |
--------------------------------------------------------------------------------
/part7/bloglist-backend/utils/db_helper.js:
--------------------------------------------------------------------------------
1 | const mongoose = require("mongoose");
2 | const config = require("./config");
3 | const logger = require("./logger");
4 |
5 | module.exports.connect = async () => {
6 | mongoose.connection.on("connected", () => {
7 | logger.info("Connection Established");
8 | });
9 |
10 | mongoose.connection.on("reconnected", () => {
11 | logger.info("Connection Reestablished");
12 | });
13 |
14 | mongoose.connection.on("disconnected", () => {
15 | logger.info("Connection Disconnected");
16 | });
17 |
18 | mongoose.connection.on("close", () => {
19 | logger.info("Connection Closed");
20 | });
21 |
22 | mongoose.connection.on("error", (error) => {
23 | logger.error(`ERROR: ${error}`);
24 | });
25 |
26 | if (process.env.NODE_ENV === "test") {
27 | return Promise.resolve("");
28 | }
29 |
30 | mongoose.set("useCreateIndex", true);
31 | mongoose.set("useFindAndModify", false);
32 |
33 | return mongoose.connect(config.MONGODB_URI, {
34 | useNewUrlParser: true,
35 | useUnifiedTopology: true,
36 | });
37 | };
38 |
--------------------------------------------------------------------------------
/part7/bloglist-backend/utils/error_helper.js:
--------------------------------------------------------------------------------
1 | const logger = require("../utils/logger");
2 |
3 | class ErrorHelper extends Error {
4 | constructor(statusCode, kind, messages = []) {
5 | super();
6 | this.statusCode = statusCode;
7 | this.kind = kind;
8 | this.messages = messages;
9 | this.message = this.messages.join("\n");
10 | }
11 | }
12 | const handleError = (err, res) => {
13 | try {
14 | const { statusCode, kind, message, messages } = err;
15 |
16 | res.status(statusCode).json({
17 | statusCode,
18 | kind,
19 | message,
20 | messages,
21 | });
22 | } catch (exception) {
23 | logger.error("IN handleError helper\n", exception.message);
24 | }
25 | };
26 |
27 | module.exports = {
28 | ErrorHelper,
29 | handleError,
30 | };
31 |
--------------------------------------------------------------------------------
/part7/bloglist-backend/utils/logger.js:
--------------------------------------------------------------------------------
1 | const info = (...params) => {
2 | if (process.env.NODE_ENV !== "test") {
3 | console.log(...params);
4 | }
5 | };
6 |
7 | const error = (...params) => {
8 | console.error(...params);
9 | };
10 |
11 | module.exports = {
12 | info,
13 | error,
14 | };
15 |
--------------------------------------------------------------------------------
/part7/bloglist-frontend/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | build
--------------------------------------------------------------------------------
/part7/bloglist-frontend/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | es6: true,
4 | node: true,
5 | "jest/globals": true,
6 | "cypress/globals": true,
7 | },
8 | extends: [
9 | "react-app",
10 | "plugin:jsx-a11y/recommended",
11 | "plugin:prettier/recommended",
12 | ],
13 | globals: {
14 | Atomics: "readonly",
15 | SharedArrayBuffer: "readonly",
16 | },
17 | parserOptions: {
18 | ecmaVersion: 2018,
19 | },
20 | plugins: ["jest", "cypress", "jsx-a11y", "prettier"],
21 | rules: {
22 | "prettier/prettier": [
23 | "error",
24 | {
25 | trailingComma: "es5",
26 | arrowParens: "always",
27 | printWidth: 80,
28 | tabWidth: 2,
29 | semi: true,
30 | singleQuote: false,
31 | bracketSpacing: true,
32 | },
33 | ],
34 | "react/jsx-filename-extension": [1, { extensions: [".js", ".jsx"] }],
35 | "no-param-reassign": ["error", { props: false }],
36 | "no-console": 0,
37 | // "react/prop-types": 0,
38 | "jsx-a11y/label-has-associated-control": [
39 | 2,
40 | {
41 | labelComponents: [],
42 | labelAttributes: [],
43 | controlComponents: ["Input"],
44 | assert: "either",
45 | depth: 3,
46 | },
47 | ],
48 | },
49 | };
50 |
--------------------------------------------------------------------------------
/part7/bloglist-frontend/README.md:
--------------------------------------------------------------------------------
1 | # Full Stack Open 2020 - Exercise Solutions - Bloglist Frontend
2 |
3 | ## Part 7 - [React router, custom hooks, styling app with CSS and webpack](https://fullstackopen.com/en/part7)
4 |
5 | Requires [bloglist-backend](https://github.com/jeremy-ebinum/full-stack-open-2020/tree/master/part7/bloglist-backend) to be running. However, tests except the cypress E2E tests can be run without it.
6 |
7 | ## Available Scripts
8 |
9 | In the project directory, you can run:
10 |
11 | ### `npm start`
12 |
13 | Runs the app in the development mode.
14 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
15 |
16 | The page will reload if you make edits.
17 |
18 | ### `npm start:test`
19 |
20 | Runs the app in test mode for the cypress E2E tests
21 |
22 | ### `npm run build`
23 |
24 | Builds the app for production to the `build` folder.
25 |
26 | ### `npm run build:analyze`
27 |
28 | Builds the app for production to the `build` folder.
29 | Also enables stats logging and webpack-bundle-analyzer
30 |
31 | ### `npm test`
32 |
33 | Launches the test runner
34 |
35 | ### `npm run test:watch`
36 |
37 | Launches the test runner in the interactive watch mode.
38 |
39 | ### `npm run cypress:open`
40 |
41 | Launches cypress for End-to-End tests
42 |
43 | ### `npm run test:coverage`
44 |
45 | Launches the test runner and generates coverage report
46 |
47 | ### `npm run lint`
48 |
49 | Lints project with eslint
50 |
--------------------------------------------------------------------------------
/part7/bloglist-frontend/__mocks__/fileMock.js:
--------------------------------------------------------------------------------
1 | module.exports = "test-file-stub";
2 |
--------------------------------------------------------------------------------
/part7/bloglist-frontend/__mocks__/styleMock.js:
--------------------------------------------------------------------------------
1 | module.exports = {};
2 |
--------------------------------------------------------------------------------
/part7/bloglist-frontend/cypress.json:
--------------------------------------------------------------------------------
1 | {
2 | "baseUrl": "http://localhost:3000"
3 | }
4 |
--------------------------------------------------------------------------------
/part7/bloglist-frontend/cypress/fixtures/blogs.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "title": "First Test Blog Title",
4 | "author": "First Test Blog Author",
5 | "url": "http://site.com"
6 | },
7 | {
8 | "title": "Second Test Blog Title",
9 | "author": "Second Test Blog Author",
10 | "url": "http://site.com"
11 | },
12 | {
13 | "title": "Third Test Blog Title",
14 | "author": "Third Test Blog Author",
15 | "url": "http://site.com"
16 | }
17 | ]
18 |
--------------------------------------------------------------------------------
/part7/bloglist-frontend/cypress/fixtures/users.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "name": "Test User",
4 | "username": "test_username",
5 | "password": "test_password"
6 | },
7 | {
8 | "name": "Other Test User",
9 | "username": "other_test_username",
10 | "password": "ohter_test_password"
11 | }
12 | ]
13 |
--------------------------------------------------------------------------------
/part7/bloglist-frontend/cypress/plugins/index.js:
--------------------------------------------------------------------------------
1 | ///
2 | // ***********************************************************
3 | // This example plugins/index.js can be used to load plugins
4 | //
5 | // You can change the location of this file or turn off loading
6 | // the plugins file with the 'pluginsFile' configuration option.
7 | //
8 | // You can read more here:
9 | // https://on.cypress.io/plugins-guide
10 | // ***********************************************************
11 |
12 | // This function is called when a project is opened or re-opened (e.g. due to
13 | // the project's config changing)
14 |
15 | /**
16 | * @type {Cypress.PluginConfig}
17 | */
18 | module.exports = (on, config) => {
19 | // `on` is used to hook into various events Cypress emits
20 | // `config` is the resolved Cypress config
21 | }
22 |
--------------------------------------------------------------------------------
/part7/bloglist-frontend/cypress/support/commands.js:
--------------------------------------------------------------------------------
1 | // ***********************************************
2 | // This example commands.js shows you how to
3 | // create various custom commands and overwrite
4 | // existing commands.
5 | //
6 | // For more comprehensive examples of custom
7 | // commands please read more here:
8 | // https://on.cypress.io/custom-commands
9 | // ***********************************************
10 | //
11 | //
12 | // -- This is a parent command --
13 | // Cypress.Commands.add("login", (email, password) => { ... })
14 | //
15 | //
16 | // -- This is a child command --
17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
18 | //
19 | //
20 | // -- This is a dual command --
21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
22 | //
23 | //
24 | // -- This will overwrite an existing command --
25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
26 |
--------------------------------------------------------------------------------
/part7/bloglist-frontend/cypress/support/index.js:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example support/index.js is processed and
3 | // loaded automatically before your test files.
4 | //
5 | // This is a great place to put global configuration and
6 | // behavior that modifies Cypress.
7 | //
8 | // You can change the location of this file or turn off
9 | // automatically serving support files with the
10 | // 'supportFile' configuration option.
11 | //
12 | // You can read more here:
13 | // https://on.cypress.io/configuration
14 | // ***********************************************************
15 |
16 | // Import commands.js using ES2015 syntax:
17 | import './commands'
18 |
19 | // Alternatively you can use CommonJS syntax:
20 | // require('./commands')
21 |
--------------------------------------------------------------------------------
/part7/bloglist-frontend/cypress/support/utils.js:
--------------------------------------------------------------------------------
1 | export const uiLogin = (username, password) => {
2 | cy.get("[data-testid='Login_username']").type(username);
3 | cy.get("[data-testid='Login_password']").type(password);
4 | cy.get("[data-testid='Login_submitButton']").click();
5 | };
6 |
7 | export const uiCreateBlog = ({ title, author, url }) => {
8 | cy.get("[data-testid='BlogForm_title']").type(title);
9 | cy.get("[data-testid='BlogForm_author']").type(author);
10 | cy.get("[data-testid='BlogForm_url']").type(url);
11 | cy.get("[data-testid='BlogForm_submitButton']").click();
12 | };
13 |
--------------------------------------------------------------------------------
/part7/bloglist-frontend/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeremy-ebinum/full-stack-open-2020/ce8ebbefb5f60c71362b12849129167348891680/part7/bloglist-frontend/public/favicon.ico
--------------------------------------------------------------------------------
/part7/bloglist-frontend/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Blog List | Full Stack Open 2019 Project
7 |
8 |
9 | You need to enable JavaScript to run this app.
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/part7/bloglist-frontend/src/components/Home.js:
--------------------------------------------------------------------------------
1 | import React, { useLayoutEffect } from "react";
2 | import NavBar from "./NavBar";
3 | import NotificationList from "./NotificationList";
4 | import BlogForm from "./BlogForm";
5 | import BlogList from "./BlogList";
6 | import ToTopScroller from "./ToTopScroller";
7 | import { getTestIDs } from "../helpers/testHelper";
8 |
9 | export const testIDs = getTestIDs();
10 |
11 | const Home = () => {
12 | useLayoutEffect(() => {
13 | const rootStyle = document.documentElement.style;
14 | document.title = "Blog List | Home";
15 |
16 | rootStyle.setProperty("--body-bg-color", "var(--light-color)");
17 | }, []);
18 |
19 | return (
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | );
33 | };
34 |
35 | export default Home;
36 |
--------------------------------------------------------------------------------
/part7/bloglist-frontend/src/components/Modal.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import PropTypes from "prop-types";
3 |
4 | const Modal = ({ testid, children }) => {
5 | useEffect(() => {
6 | const rootStyle = document.documentElement.style;
7 | const wrapper = document.querySelector(".js-wrapper");
8 | rootStyle.setProperty("--body-overflow", "hidden");
9 | wrapper.classList.add("hasModal");
10 | return () => {
11 | rootStyle.setProperty("--body-overflow", "auto");
12 | wrapper.classList.remove("hasModal");
13 | };
14 | }, []);
15 |
16 | return (
17 |
20 | );
21 | };
22 |
23 | Modal.propTypes = {
24 | children: PropTypes.node.isRequired,
25 | testid: PropTypes.string,
26 | };
27 |
28 | Modal.defaultProps = {
29 | testid: null,
30 | };
31 |
32 | export default Modal;
33 |
--------------------------------------------------------------------------------
/part7/bloglist-frontend/src/components/ModalSpinner.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
3 | import { faSpinner } from "@fortawesome/free-solid-svg-icons/faSpinner";
4 | import { getTestIDs } from "../helpers/testHelper";
5 | import Modal from "./Modal";
6 |
7 | export const testIDs = getTestIDs();
8 |
9 | const ModalSpinner = () => {
10 | return (
11 |
12 |
17 |
18 | );
19 | };
20 |
21 | export default ModalSpinner;
22 |
--------------------------------------------------------------------------------
/part7/bloglist-frontend/src/components/NotFound.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const NotFound = () => {
4 | return (
5 |
6 |
Not Found
7 |
8 | Sorry, The Page You Are Looking For Does Not Exist
9 |
10 |
11 | );
12 | };
13 |
14 | export default NotFound;
15 |
--------------------------------------------------------------------------------
/part7/bloglist-frontend/src/components/NotFoundPage.js:
--------------------------------------------------------------------------------
1 | import React, { useLayoutEffect } from "react";
2 | import NavBar from "./NavBar";
3 | import NotificationList from "./NotificationList";
4 |
5 | const NotFound = () => {
6 | useLayoutEffect(() => {
7 | const rootStyle = document.documentElement.style;
8 | document.title = "Blog List | Not Found";
9 |
10 | rootStyle.setProperty("--body-bg-color", "var(--light-color)");
11 | }, []);
12 |
13 | return (
14 |
15 |
16 |
17 |
18 |
19 |
Not Found
20 |
21 | Sorry, The Page You Are Looking For Does Not Exist
22 |
23 |
24 |
25 |
26 | );
27 | };
28 |
29 | export default NotFound;
30 |
--------------------------------------------------------------------------------
/part7/bloglist-frontend/src/components/Notification.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import { getTestIDs } from "../helpers/testHelper";
4 |
5 | export const testIDs = getTestIDs();
6 |
7 | const alertTypes = {
8 | error: "c-alert c-alert--error",
9 | success: "c-alert c-alert--success",
10 | info: "c-alert c-alert--info",
11 | default: "c-alert",
12 | };
13 |
14 | const Notification = ({ id, type, message }) => {
15 | return (
16 |
20 | {message}
21 |
22 | );
23 | };
24 |
25 | Notification.propTypes = {
26 | id: PropTypes.string.isRequired,
27 | type: PropTypes.string.isRequired,
28 | message: PropTypes.string.isRequired,
29 | };
30 |
31 | export default Notification;
32 |
--------------------------------------------------------------------------------
/part7/bloglist-frontend/src/components/PrivateRoute.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { connect } from "react-redux";
3 | import { Route, Redirect } from "react-router-dom";
4 |
5 | const PrivateRoute = ({ isAuth, ...routeProps }) => {
6 | return (
7 | <>
8 | {!isAuth && (
9 |
15 | )}
16 |
17 | {isAuth && }
18 | >
19 | );
20 | };
21 |
22 | const mapStateToProps = (state, ownProps) => {
23 | const { isAuth } = state.auth;
24 |
25 | return { isAuth, ...ownProps };
26 | };
27 |
28 | export default connect(mapStateToProps)(PrivateRoute);
29 |
--------------------------------------------------------------------------------
/part7/bloglist-frontend/src/components/PublicRoute.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { connect } from "react-redux";
3 | import { Route, Redirect } from "react-router-dom";
4 |
5 | const PublicRoute = ({ isAuth, restricted, ...routeProps }) => {
6 | return (
7 | <>
8 | {isAuth && restricted ? : }
9 | >
10 | );
11 | };
12 |
13 | const mapStateToProps = (state, ownProps) => {
14 | const { isAuth } = state.auth;
15 |
16 | return { isAuth, ...ownProps };
17 | };
18 |
19 | export default connect(mapStateToProps)(PublicRoute);
20 |
--------------------------------------------------------------------------------
/part7/bloglist-frontend/src/configureStore.js:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware, compose } from "redux";
2 | import { composeWithDevTools } from "redux-devtools-extension";
3 | import createRootReducer from "./reducers";
4 | import thunk from "redux-thunk";
5 | import { createBrowserHistory } from "history";
6 | import { routerMiddleware } from "connected-react-router";
7 |
8 | export const history = createBrowserHistory();
9 |
10 | const configureStore = (preloadedState) => {
11 | let store;
12 |
13 | if (process.env.NODE_ENV === "development") {
14 | store = createStore(
15 | createRootReducer(history),
16 | preloadedState,
17 | composeWithDevTools(applyMiddleware(routerMiddleware(history), thunk))
18 | );
19 | } else {
20 | store = createStore(
21 | createRootReducer(history),
22 | preloadedState,
23 | compose(applyMiddleware(routerMiddleware(history), thunk))
24 | );
25 | }
26 |
27 | return store;
28 | };
29 |
30 | export default configureStore;
31 |
--------------------------------------------------------------------------------
/part7/bloglist-frontend/src/fonts/Dosis-Light.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeremy-ebinum/full-stack-open-2020/ce8ebbefb5f60c71362b12849129167348891680/part7/bloglist-frontend/src/fonts/Dosis-Light.woff2
--------------------------------------------------------------------------------
/part7/bloglist-frontend/src/fonts/Dosis-Medium.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeremy-ebinum/full-stack-open-2020/ce8ebbefb5f60c71362b12849129167348891680/part7/bloglist-frontend/src/fonts/Dosis-Medium.woff2
--------------------------------------------------------------------------------
/part7/bloglist-frontend/src/fonts/Roboto-Bold-webfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeremy-ebinum/full-stack-open-2020/ce8ebbefb5f60c71362b12849129167348891680/part7/bloglist-frontend/src/fonts/Roboto-Bold-webfont.woff
--------------------------------------------------------------------------------
/part7/bloglist-frontend/src/fonts/Roboto-Regular-webfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeremy-ebinum/full-stack-open-2020/ce8ebbefb5f60c71362b12849129167348891680/part7/bloglist-frontend/src/fonts/Roboto-Regular-webfont.woff
--------------------------------------------------------------------------------
/part7/bloglist-frontend/src/hooks/index.js:
--------------------------------------------------------------------------------
1 | import { useState, useCallback } from "react";
2 |
3 | export const useField = ({
4 | id = null,
5 | className = "c-row__input",
6 | type = "text",
7 | name = null,
8 | placeholder = null,
9 | }) => {
10 | const [value, setValue] = useState("");
11 |
12 | const onChange = useCallback((event) => {
13 | setValue(event.target.value);
14 | }, []);
15 |
16 | const reset = useCallback(() => setValue(""), []);
17 |
18 | return [{ id, className, type, name, placeholder, value, onChange }, reset];
19 | };
20 |
--------------------------------------------------------------------------------
/part7/bloglist-frontend/src/index.js:
--------------------------------------------------------------------------------
1 | /* istanbul ignore file */
2 | import React from "react";
3 | import ReactDOM from "react-dom";
4 | import { AppContainer } from "react-hot-loader";
5 | import configureStore, { history } from "./configureStore";
6 | import { Provider } from "react-redux";
7 | import App from "./App";
8 | import "./index.css";
9 |
10 | if (process.env.NODE_ENV === "development") {
11 | const whyDidYouRender = require("@welldone-software/why-did-you-render");
12 | whyDidYouRender(React);
13 | }
14 |
15 | const store = configureStore();
16 |
17 | const render = () => {
18 | ReactDOM.render(
19 |
20 |
21 |
22 |
23 | ,
24 | document.getElementById("root")
25 | );
26 | };
27 |
28 | render();
29 |
30 | if (module.hot) {
31 | module.hot.accept("./App", () => {
32 | render();
33 | });
34 | }
35 |
--------------------------------------------------------------------------------
/part7/bloglist-frontend/src/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from "redux";
2 | import { connectRouter } from "connected-react-router";
3 |
4 | import notificationReducer from "./notificationReducer";
5 | import requestReducer from "./requestReducer";
6 | import blogReducer from "./blogReducer";
7 | import authReducer from "./authReducer";
8 | import userReducer from "./userReducer";
9 |
10 | const createRootReducer = (history) =>
11 | combineReducers({
12 | router: connectRouter(history),
13 | notifications: notificationReducer,
14 | requests: requestReducer,
15 | blogs: blogReducer,
16 | users: userReducer,
17 | auth: authReducer,
18 | });
19 |
20 | export default createRootReducer;
21 |
--------------------------------------------------------------------------------
/part7/bloglist-frontend/src/reducers/userReducer.js:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 |
3 | import usersService from "../services/users";
4 | import { setRequestState } from "./requestReducer";
5 | import { displayNotification } from "./notificationReducer";
6 |
7 | import { logger } from "../utils";
8 |
9 | export const initialState = [];
10 |
11 | const userReducer = (state = initialState, action) => {
12 | switch (action.type) {
13 | case "INIT_USERS":
14 | if (action.data && !Array.isArray(action.data)) {
15 | throw new Error("Users should be passed as an array");
16 | }
17 | return action.data;
18 | default:
19 | return state;
20 | }
21 | };
22 |
23 | export const initUsers = () => {
24 | return async (dispatch, getState) => {
25 | try {
26 | const source = await getState().requests.initUsers.source;
27 | dispatch(setRequestState("initUsers", "LOADING"));
28 | const users = await usersService.getAll(source.token);
29 |
30 | dispatch(setRequestState("initUsers", "SUCCESS"));
31 | dispatch({ type: "INIT_USERS", data: users });
32 | } catch (e) {
33 | if (!axios.isCancel(e)) {
34 | console.log(e.message);
35 | logger.error(e);
36 | dispatch(setRequestState("initUsers", "FAILURE"));
37 | dispatch(displayNotification("Oops! Something went wrong", "error"));
38 | }
39 | }
40 | };
41 | };
42 |
43 | export default userReducer;
44 |
--------------------------------------------------------------------------------
/part7/bloglist-frontend/src/services/blogs.js:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 |
3 | const baseUrl = "/api/blogs";
4 |
5 | let token = null;
6 |
7 | const setToken = (newToken) => {
8 | if (newToken) {
9 | token = `Bearer ${newToken}`;
10 | } else {
11 | token = null;
12 | }
13 | };
14 |
15 | const create = async (newBlog) => {
16 | const config = { headers: { Authorization: token } };
17 | const response = await axios.post(baseUrl, newBlog, config);
18 |
19 | return response.data;
20 | };
21 |
22 | const getAll = async (cancelToken) => {
23 | const response = await axios.get(baseUrl, { cancelToken });
24 |
25 | return response.data;
26 | };
27 |
28 | const update = async (id, updatedBlog) => {
29 | const config = { headers: { Authorization: token } };
30 | const response = await axios.put(`${baseUrl}/${id}`, updatedBlog, config);
31 |
32 | return response.data;
33 | };
34 |
35 | const remove = async (id) => {
36 | const config = { headers: { Authorization: token } };
37 | const response = await axios.delete(`${baseUrl}/${id}`, config);
38 |
39 | return response.data;
40 | };
41 |
42 | const comment = async (id, comment) => {
43 | const config = {
44 | headers: { Authorization: token, "Content-Type": "text/plain" },
45 | };
46 | const response = await axios.post(
47 | `${baseUrl}/${id}/comments`,
48 | comment,
49 | config
50 | );
51 |
52 | return response.data;
53 | };
54 |
55 | export default { setToken, create, getAll, update, remove, comment };
56 |
--------------------------------------------------------------------------------
/part7/bloglist-frontend/src/services/login.js:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 |
3 | const baseUrl = "/api/login";
4 |
5 | const login = async (credentials) => {
6 | const response = await axios.post(baseUrl, credentials);
7 |
8 | return response.data;
9 | };
10 |
11 | export default { login };
12 |
--------------------------------------------------------------------------------
/part7/bloglist-frontend/src/services/users.js:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 |
3 | const baseUrl = "/api/users";
4 |
5 | const getAll = async (cancelToken) => {
6 | const response = await axios.get(baseUrl, { cancelToken });
7 |
8 | return response.data;
9 | };
10 |
11 | export default { getAll };
12 |
--------------------------------------------------------------------------------
/part7/bloglist-frontend/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import "@testing-library/jest-dom/extend-expect";
6 |
7 | /* Mocks
8 | *******************************************************************************/
9 | // localStorage
10 | let savedItems = {};
11 |
12 | const localStorageMock = {
13 | setItem: (key, item) => {
14 | savedItems[key] = item;
15 | },
16 | getItem: (key) => savedItems[key],
17 | removeItem: (key) => delete savedItems[key],
18 | clear: () => {
19 | savedItems = {};
20 | },
21 | };
22 |
23 | global.localStorage = localStorageMock;
24 |
25 | // window.confirm
26 | window.confirm = () => true;
27 |
--------------------------------------------------------------------------------
/part7/bloglist-frontend/src/utils/index.js:
--------------------------------------------------------------------------------
1 | /* istanbul ignore file */
2 | import axios from "axios";
3 |
4 | export const getTrimmedStr = (str) => {
5 | return str.length > 50 ? str.slice(0, 49) + "..." : str;
6 | };
7 |
8 | export const getCancelTokenSource = () => {
9 | const CancelToken = axios.CancelToken;
10 | const source = CancelToken.source();
11 |
12 | return source;
13 | };
14 |
15 | export const logger = {
16 | info: function(...params) {
17 | if (process.env.NODE_ENV !== "test") {
18 | console.log(...params);
19 | }
20 | },
21 |
22 | error: function(...params) {
23 | console.error(...params);
24 | },
25 |
26 | test: function(...params) {
27 | if (process.env.NODE_ENV === "test") {
28 | console.log(...params);
29 | }
30 | },
31 | };
32 |
--------------------------------------------------------------------------------
/part7/country-hook/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "countryhook",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^4.2.4",
7 | "@testing-library/react": "^9.4.0",
8 | "@testing-library/user-event": "^7.2.1",
9 | "axios": "^0.19.2",
10 | "react": "^16.12.0",
11 | "react-dom": "^16.12.0",
12 | "react-scripts": "3.4.0"
13 | },
14 | "scripts": {
15 | "start": "react-scripts start",
16 | "build": "react-scripts build",
17 | "test": "react-scripts test",
18 | "eject": "react-scripts eject"
19 | },
20 | "eslintConfig": {
21 | "extends": "react-app"
22 | },
23 | "browserslist": {
24 | "production": [
25 | ">0.2%",
26 | "not dead",
27 | "not op_mini all"
28 | ],
29 | "development": [
30 | "last 1 chrome version",
31 | "last 1 firefox version",
32 | "last 1 safari version"
33 | ]
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/part7/country-hook/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeremy-ebinum/full-stack-open-2020/ce8ebbefb5f60c71362b12849129167348891680/part7/country-hook/public/favicon.ico
--------------------------------------------------------------------------------
/part7/country-hook/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeremy-ebinum/full-stack-open-2020/ce8ebbefb5f60c71362b12849129167348891680/part7/country-hook/public/logo192.png
--------------------------------------------------------------------------------
/part7/country-hook/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeremy-ebinum/full-stack-open-2020/ce8ebbefb5f60c71362b12849129167348891680/part7/country-hook/public/logo512.png
--------------------------------------------------------------------------------
/part7/country-hook/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/part7/country-hook/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/part7/country-hook/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import App from './App'
4 |
5 | ReactDOM.render( , document.getElementById('root'))
--------------------------------------------------------------------------------
/part7/routed-anecdotes/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | build
--------------------------------------------------------------------------------
/part7/routed-anecdotes/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | es6: true,
4 | node: true,
5 | },
6 | extends: ["react-app", "plugin:prettier/recommended"],
7 | globals: {
8 | Atomics: "readonly",
9 | SharedArrayBuffer: "readonly",
10 | },
11 | parserOptions: {
12 | ecmaVersion: 2018,
13 | },
14 | plugins: ["prettier"],
15 | rules: {
16 | "prettier/prettier": [
17 | "error",
18 | {
19 | trailingComma: "es5",
20 | arrowParens: "always",
21 | printWidth: 80,
22 | tabWidth: 2,
23 | semi: true,
24 | singleQuote: false,
25 | bracketSpacing: true,
26 | },
27 | ],
28 | "react/jsx-filename-extension": [1, { extensions: [".js", ".jsx"] }],
29 | "no-param-reassign": ["error", { props: false }],
30 | "no-console": 0,
31 | },
32 | };
33 |
--------------------------------------------------------------------------------
/part7/routed-anecdotes/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "routed-anecdotes",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "react": "^16.12.0",
7 | "react-dom": "^16.12.0",
8 | "react-router-dom": "^5.1.2",
9 | "react-scripts": "3.4.0"
10 | },
11 | "devDependencies": {
12 | "eslint-config-prettier": "^6.10.0",
13 | "eslint-plugin-prettier": "^3.1.2",
14 | "prettier": "^1.19.1"
15 | },
16 | "scripts": {
17 | "start": "react-scripts start",
18 | "build": "react-scripts build",
19 | "test": "react-scripts test",
20 | "eject": "react-scripts eject"
21 | },
22 | "eslintConfig": {
23 | "extends": "react-app"
24 | },
25 | "browserslist": {
26 | "production": [
27 | ">0.2%",
28 | "not dead",
29 | "not op_mini all"
30 | ],
31 | "development": [
32 | "last 1 chrome version",
33 | "last 1 firefox version",
34 | "last 1 safari version"
35 | ]
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/part7/routed-anecdotes/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeremy-ebinum/full-stack-open-2020/ce8ebbefb5f60c71362b12849129167348891680/part7/routed-anecdotes/public/favicon.ico
--------------------------------------------------------------------------------
/part7/routed-anecdotes/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/part7/routed-anecdotes/src/components/About.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const About = () => (
4 |
5 |
About anecdote app
6 |
According to Wikipedia:
7 |
8 |
9 | An anecdote is a brief, revealing account of an individual person or an
10 | incident. Occasionally humorous, anecdotes differ from jokes because their
11 | primary purpose is not simply to provoke laughter but to reveal a truth
12 | more general than the brief tale itself, such as to characterize a person
13 | by delineating a specific quirk or trait, to communicate an abstract idea
14 | about a person, place, or thing through the concrete details of a short
15 | narrative. An anecdote is "a story with a point."
16 |
17 |
18 |
19 | Software engineering is full of excellent anecdotes, at this app you can
20 | find the best and add more.
21 |
22 |
23 | );
24 |
25 | export default About;
26 |
--------------------------------------------------------------------------------
/part7/routed-anecdotes/src/components/Anecdote.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const Anecdote = ({ anecdote, vote }) => (
4 | <>
5 | {!anecdote && Anecdote does not exist
}
6 |
7 | {anecdote && (
8 |
9 |
10 | {anecdote.content} by {anecdote.author}
11 |
12 |
13 | has {anecdote.votes} {anecdote.votes === 1 ? "vote" : "votes"}
14 | vote(anecdote.id)}>
15 | vote
16 |
17 |
18 |
19 | for more info see{" "}
20 |
21 | {anecdote.info}
22 |
23 |
24 |
25 | )}
26 | >
27 | );
28 |
29 | export default Anecdote;
30 |
--------------------------------------------------------------------------------
/part7/routed-anecdotes/src/components/AnecdoteList.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Link } from "react-router-dom";
3 |
4 | const AnecdoteList = ({ anecdotes }) => (
5 |
6 |
Anecdotes
7 |
8 | {anecdotes.map((anecdote) => (
9 |
10 | {anecdote.content}
11 |
12 | ))}
13 |
14 |
15 | );
16 |
17 | export default AnecdoteList;
18 |
--------------------------------------------------------------------------------
/part7/routed-anecdotes/src/components/CreateNew.js:
--------------------------------------------------------------------------------
1 | import React, { useCallback } from "react";
2 | import { useHistory } from "react-router-dom";
3 | import { useField } from "../hooks";
4 |
5 | const CreateNew = ({ addNew }) => {
6 | const [content, resetContent] = useField();
7 | const [author, resetAuthor] = useField();
8 | const [info, resetInfo] = useField({ type: "url" });
9 |
10 | const history = useHistory();
11 |
12 | const resetForm = useCallback(() => {
13 | resetContent();
14 | resetAuthor();
15 | resetInfo();
16 | }, [resetContent, resetAuthor, resetInfo]);
17 |
18 | const handleSubmit = (e) => {
19 | e.preventDefault();
20 | addNew({
21 | content: content.value,
22 | author: author.value,
23 | info: info.value,
24 | votes: 0,
25 | });
26 | resetForm();
27 | history.push("/");
28 | };
29 |
30 | return (
31 |
32 |
create a new anecdote
33 |
51 |
52 | );
53 | };
54 |
55 | export default CreateNew;
56 |
--------------------------------------------------------------------------------
/part7/routed-anecdotes/src/components/Footer.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const Footer = () => (
4 |
16 | );
17 |
18 | export default Footer;
19 |
--------------------------------------------------------------------------------
/part7/routed-anecdotes/src/components/Menu.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Link } from "react-router-dom";
3 |
4 | const Menu = () => {
5 | const padding = {
6 | paddingRight: 5,
7 | };
8 | return (
9 |
10 |
11 | Anecdotes
12 |
13 |
14 | Create New
15 |
16 |
17 | About
18 |
19 |
20 | );
21 | };
22 |
23 | export default Menu;
24 |
--------------------------------------------------------------------------------
/part7/routed-anecdotes/src/hooks/index.js:
--------------------------------------------------------------------------------
1 | import { useState, useCallback } from "react";
2 |
3 | export const useField = ({ type = "text" } = {}) => {
4 | const [value, setValue] = useState("");
5 |
6 | const onChange = useCallback((event) => {
7 | setValue(event.target.value);
8 | }, []);
9 |
10 | const reset = useCallback(() => setValue(""), []);
11 |
12 | return [{ type, value, onChange }, reset];
13 | };
14 |
--------------------------------------------------------------------------------
/part7/routed-anecdotes/src/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import { BrowserRouter as Router } from "react-router-dom";
4 | import App from "./App";
5 |
6 | ReactDOM.render(
7 |
8 |
9 | ,
10 | document.getElementById("root")
11 | );
12 |
--------------------------------------------------------------------------------
/part7/ultimate-hooks/README.md:
--------------------------------------------------------------------------------
1 | # Full Stack Open 2020 - Exercise Solutions - Ultimate Hooks
2 |
3 | ## Part 7 - [React router, custom hooks, styling app with CSS and webpack](https://fullstackopen.com/en/part7)
4 |
5 | ## usage
6 |
7 | Start server to port 3005 with _npm run server_
8 |
9 | Run frontend in development mode with _npm start_
10 |
--------------------------------------------------------------------------------
/part7/ultimate-hooks/db.json:
--------------------------------------------------------------------------------
1 | {
2 | "notes": [
3 | {
4 | "content": "custom-hooks are awesome!",
5 | "id": 1
6 | },
7 | {
8 | "content": "best feature ever <3",
9 | "id": 2
10 | },
11 | {
12 | "content": "ultimate-hooks is now online",
13 | "id": 3
14 | },
15 | {
16 | "content": "SPAAAAAAMMMMM",
17 | "id": 4
18 | },
19 | {
20 | "content": "EGGSSSSSSSSSS",
21 | "id": 5
22 | }
23 | ],
24 | "persons": [
25 | {
26 | "name": "mluukkai",
27 | "number": "040-5483923",
28 | "id": 1
29 | },
30 | {
31 | "name": "jeremy ebinum",
32 | "number": "2348000000000",
33 | "id": 2
34 | }
35 | ]
36 | }
--------------------------------------------------------------------------------
/part7/ultimate-hooks/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ultimate-hooks",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^4.2.4",
7 | "@testing-library/react": "^9.4.0",
8 | "@testing-library/user-event": "^7.2.1",
9 | "axios": "^0.19.2",
10 | "react": "^16.12.0",
11 | "react-dom": "^16.12.0",
12 | "react-scripts": "3.4.0"
13 | },
14 | "scripts": {
15 | "start": "react-scripts start",
16 | "build": "react-scripts build",
17 | "test": "react-scripts test",
18 | "eject": "react-scripts eject",
19 | "server": "json-server --port=3005 --watch db.json"
20 | },
21 | "eslintConfig": {
22 | "extends": "react-app"
23 | },
24 | "browserslist": {
25 | "production": [
26 | ">0.2%",
27 | "not dead",
28 | "not op_mini all"
29 | ],
30 | "development": [
31 | "last 1 chrome version",
32 | "last 1 firefox version",
33 | "last 1 safari version"
34 | ]
35 | },
36 | "devDependencies": {
37 | "json-server": "^0.15.1"
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/part7/ultimate-hooks/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeremy-ebinum/full-stack-open-2020/ce8ebbefb5f60c71362b12849129167348891680/part7/ultimate-hooks/public/favicon.ico
--------------------------------------------------------------------------------
/part7/ultimate-hooks/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/part7/ultimate-hooks/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import App from './App'
4 |
5 | ReactDOM.render( , document.getElementById('root'))
6 |
--------------------------------------------------------------------------------
/part8/README.md:
--------------------------------------------------------------------------------
1 | # Full Stack Open 2020 - Exercise Solutions
2 |
3 | ## Part 8 - [GraphQL](https://fullstackopen.com/en/part8)
4 |
5 | Each of the following folders contain the final solutions to their respective exercises
6 |
7 | ### [library-backend](https://github.com/jeremy-ebinum/full-stack-open-2020/tree/master/part8/library-backend)
8 |
9 | - Exercises 8.1 - 8.7 — [Description](https://fullstackopen.com/en/part8/graph_ql_server#exercises-8-1-8-7)
10 | - Exercises 8.13 - 8.16 — [Description](https://fullstackopen.com/en/part8/database_and_user_administration#exercises-8-13-8-16)
11 | - Exercises 8.23 - 8.26 — [Description](https://fullstackopen.com/en/part8/fragments_and_subscriptions#exercises-8-23-8-26)
12 |
13 | ### [library-frontend](https://github.com/jeremy-ebinum/full-stack-open-2020/tree/master/part8/library-frontend)
14 |
15 | - Exercises 8.8 - 8.12 — [Description](https://fullstackopen.com/en/part8/react_and_graph_ql#exercises-8-8-8-12)
16 | - Exercises 8.13 - 8.16 — [Description](https://fullstackopen.com/en/part8/database_and_user_administration#exercises-8-13-8-16)
17 | - Exercises 8.17 - 8.22 — [Description](https://fullstackopen.com/en/part8/login_and_updating_the_cache#exercises-8-17-8-22)
18 | - Exercises 8.23 - 8.26 — [Description](https://fullstackopen.com/en/part8/fragments_and_subscriptions#exercises-8-23-8-26)
19 |
--------------------------------------------------------------------------------
/part8/library-backend/.env.example:
--------------------------------------------------------------------------------
1 | MONGODB_URI=mongodb://[username:password@]host1[:port1][,...hostN[:portN]][/[database][?options]]
2 | JWT_SECRET=uh2e3q1!h5vq_re7ea_b4x4$-rbwy$iad!adf$g7ep5(wpg+^3
--------------------------------------------------------------------------------
/part8/library-backend/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | commonjs: true,
4 | es6: true,
5 | node: true,
6 | jest: true,
7 | },
8 | extends: ["airbnb-base", "plugin:prettier/recommended"],
9 | globals: {
10 | Atomics: "readonly",
11 | SharedArrayBuffer: "readonly",
12 | },
13 | parserOptions: {
14 | ecmaVersion: 2018,
15 | },
16 | plugins: ["prettier"],
17 | rules: {
18 | "prettier/prettier": [
19 | "error",
20 | {
21 | trailingComma: "es5",
22 | arrowParens: "always",
23 | printWidth: 80,
24 | tabWidth: 2,
25 | semi: true,
26 | singleQuote: false,
27 | bracketSpacing: true,
28 | },
29 | ],
30 | "no-console": 0,
31 | "no-param-reassign": ["error", { props: false }],
32 | "no-underscore-dangle": [
33 | "error",
34 | { allow: ["_id", "__v", "_countBy", "_merge"] },
35 | ],
36 | },
37 | };
38 |
--------------------------------------------------------------------------------
/part8/library-backend/README.md:
--------------------------------------------------------------------------------
1 | # Full Stack Open 2020 - Exercise Solutions - Library Backend
2 |
3 | ## Part 8 - [GraphQL](https://fullstackopen.com/en/part8)
4 |
5 | graphql powered server for the [library-frontend](https://github.com/jeremy-ebinum/full-stack-open-2020/tree/master/part8/library-frontend) project
6 |
7 | ## usage
8 |
9 | See .env.example for required env variables
10 |
11 | Start the server with _npm start_
12 |
13 | Start the server in watch mode with _npm run watch_
14 |
--------------------------------------------------------------------------------
/part8/library-backend/graphql/loaders.js:
--------------------------------------------------------------------------------
1 | const DataLoader = require("dataloader");
2 | const _countBy = require("lodash.countby");
3 | const Book = require("../models/Book");
4 |
5 | const createBookCountLoader = () => {
6 | return new DataLoader(async (authorIds) => {
7 | const books = await Book.find({});
8 | const booksByAuthorId = books.map((b) => b.author);
9 | const authorIdCounts = _countBy(booksByAuthorId, (id) => id);
10 |
11 | return authorIds.map((id) => authorIdCounts[id] || 0);
12 | });
13 | };
14 |
15 | module.exports = { createBookCountLoader };
16 |
--------------------------------------------------------------------------------
/part8/library-backend/graphql/resolvers.js:
--------------------------------------------------------------------------------
1 | const _merge = require("lodash.merge");
2 |
3 | const { resolvers: authorResolvers } = require("./schema/author");
4 | const { resolvers: bookResolvers } = require("./schema/book");
5 | const { resolvers: userResolvers } = require("./schema/user");
6 |
7 | const resolvers = _merge({}, authorResolvers, bookResolvers, userResolvers);
8 |
9 | module.exports = resolvers;
10 |
--------------------------------------------------------------------------------
/part8/library-backend/graphql/schema/root.js:
--------------------------------------------------------------------------------
1 | const { gql } = require("apollo-server");
2 |
3 | const typeDef = gql`
4 | type Query {
5 | _root: String
6 | }
7 |
8 | type Mutation {
9 | _root: String
10 | }
11 |
12 | type Subscription {
13 | _root: String
14 | }
15 | `;
16 |
17 | module.exports = { typeDef };
18 |
--------------------------------------------------------------------------------
/part8/library-backend/graphql/typeDefs.js:
--------------------------------------------------------------------------------
1 | const { typeDef: base } = require("./schema/root");
2 | const { typeDef: Author } = require("./schema/author");
3 | const { typeDef: Book } = require("./schema/book");
4 | const { typeDef: User } = require("./schema/user");
5 |
6 | const typeDefs = [base, Author, Book, User];
7 |
8 | module.exports = typeDefs;
9 |
--------------------------------------------------------------------------------
/part8/library-backend/helpers/errorHelper.js:
--------------------------------------------------------------------------------
1 | const { ApolloError } = require("apollo-server");
2 |
3 | module.exports.getModelValidationErrors = ({ errors }, model) => {
4 | const validationErrors = Object.values(errors).map((error) => {
5 | const path = error.path === "born" ? "birth year" : error.path;
6 | if (error.name === "CastError") {
7 | return `Invalid ${model} ${path}`;
8 | }
9 | return error.message;
10 | });
11 |
12 | return validationErrors;
13 | };
14 |
15 | module.exports.useFallbackErrorHandler = (error) => {
16 | throw new ApolloError(error.message, "INTERNAL_SERVER_ERROR");
17 | };
18 |
--------------------------------------------------------------------------------
/part8/library-backend/models/Author.js:
--------------------------------------------------------------------------------
1 | const mongoose = require("mongoose");
2 | const uniqueValidator = require("mongoose-unique-validator");
3 |
4 | const schema = new mongoose.Schema({
5 | name: {
6 | type: String,
7 | required: [true, "Authors must have a name"],
8 | unique: true,
9 | uniqueCaseInsensitive: true,
10 | minlength: [4, "Author name must be at least 4 characters long"],
11 | },
12 | born: {
13 | type: Number,
14 | },
15 | });
16 |
17 | schema.plugin(uniqueValidator, {
18 | message: "An Author with that {PATH} already exists",
19 | });
20 |
21 | module.exports = mongoose.model("Author", schema);
22 |
--------------------------------------------------------------------------------
/part8/library-backend/models/Book.js:
--------------------------------------------------------------------------------
1 | const mongoose = require("mongoose");
2 | const uniqueValidator = require("mongoose-unique-validator");
3 |
4 | const schema = new mongoose.Schema({
5 | title: {
6 | type: String,
7 | required: [true, "Books must have a title"],
8 | unique: true,
9 | uniqueCaseInsensitive: true,
10 | minlength: [2, "Book title must be at least 2 characters long"],
11 | },
12 | published: {
13 | type: Number,
14 | },
15 | author: {
16 | type: mongoose.Schema.Types.ObjectId,
17 | ref: "Author",
18 | },
19 | genres: [{ type: String }],
20 | });
21 |
22 | schema.plugin(uniqueValidator, {
23 | message: "A Book with that {PATH} already exists",
24 | });
25 |
26 | module.exports = mongoose.model("Book", schema);
27 |
--------------------------------------------------------------------------------
/part8/library-backend/models/User.js:
--------------------------------------------------------------------------------
1 | const mongoose = require("mongoose");
2 | const uniqueValidator = require("mongoose-unique-validator");
3 |
4 | const schema = new mongoose.Schema({
5 | username: {
6 | type: String,
7 | required: [true, "User must have a username"],
8 | unique: true,
9 | minlength: [3, "Username must be at least 3 characters long"],
10 | },
11 | favoriteGenre: {
12 | type: String,
13 | required: [true, "User must have a favorite genre"],
14 | minlength: [3, "User favorite genre must be at least 3 characters long"],
15 | },
16 | });
17 |
18 | schema.plugin(uniqueValidator, {
19 | message: "A User with that {PATH} already exists",
20 | });
21 |
22 | module.exports = mongoose.model("User", schema);
23 |
--------------------------------------------------------------------------------
/part8/library-backend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "library-backend",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "node index.js",
8 | "watch": "nodemon index.js",
9 | "test": "echo \"Error: no test specified\" && exit 1"
10 | },
11 | "keywords": [],
12 | "author": "Jeremy Ebinum",
13 | "license": "MIT",
14 | "dependencies": {
15 | "apollo-server": "^2.11.0",
16 | "dataloader": "^2.0.0",
17 | "dotenv": "^8.2.0",
18 | "graphql": "^14.6.0",
19 | "jsonwebtoken": "^8.5.1",
20 | "lodash.countby": "^4.6.0",
21 | "lodash.merge": "^4.6.2",
22 | "mongoose": "^5.8.11",
23 | "mongoose-unique-validator": "^2.0.3"
24 | },
25 | "devDependencies": {
26 | "eslint": "^6.8.0",
27 | "eslint-config-airbnb-base": "^14.0.0",
28 | "eslint-config-prettier": "^6.10.0",
29 | "eslint-plugin-import": "^2.20.1",
30 | "eslint-plugin-prettier": "^3.1.2",
31 | "nodemon": "^2.0.2",
32 | "prettier": "^1.19.1"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/part8/library-backend/utils/config.js:
--------------------------------------------------------------------------------
1 | require("dotenv").config();
2 |
3 | const { MONGODB_URI, JWT_SECRET } = process.env;
4 |
5 | module.exports = { MONGODB_URI, JWT_SECRET };
6 |
--------------------------------------------------------------------------------
/part8/library-backend/utils/logger.js:
--------------------------------------------------------------------------------
1 | const info = (...params) => {
2 | if (process.env.NODE_ENV !== "test") {
3 | console.log(...params);
4 | }
5 | };
6 |
7 | const error = (...params) => {
8 | console.error(...params);
9 | };
10 |
11 | module.exports = {
12 | info,
13 | error,
14 | };
15 |
--------------------------------------------------------------------------------
/part8/library-frontend/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeremy-ebinum/full-stack-open-2020/ce8ebbefb5f60c71362b12849129167348891680/part8/library-frontend/public/favicon.ico
--------------------------------------------------------------------------------
/part8/library-frontend/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeremy-ebinum/full-stack-open-2020/ce8ebbefb5f60c71362b12849129167348891680/part8/library-frontend/public/logo192.png
--------------------------------------------------------------------------------
/part8/library-frontend/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeremy-ebinum/full-stack-open-2020/ce8ebbefb5f60c71362b12849129167348891680/part8/library-frontend/public/logo512.png
--------------------------------------------------------------------------------
/part8/library-frontend/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/part8/library-frontend/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/part8/library-frontend/src/components/AuthorsTable.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 |
4 | import Table from "react-bootstrap/Table";
5 |
6 | const AuthorsTable = ({ authors }) => {
7 | return (
8 |
9 | List of Authors
10 |
11 |
12 | Name
13 | Birth Year
14 | Books
15 |
16 |
17 |
18 | {authors.map((a) => (
19 |
20 | {a.name}
21 | {a.born}
22 | {a.bookCount}
23 |
24 | ))}
25 |
26 |
27 | );
28 | };
29 |
30 | AuthorsTable.propTypes = {
31 | authors: PropTypes.arrayOf(PropTypes.object),
32 | };
33 |
34 | export default AuthorsTable;
35 |
--------------------------------------------------------------------------------
/part8/library-frontend/src/components/BooksTable.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 |
4 | import Table from "react-bootstrap/Table";
5 |
6 | const BooksTable = ({ books }) => {
7 | return (
8 |
9 | List of Books
10 |
11 |
12 | Title
13 | Author
14 | Published
15 |
16 |
17 |
18 | {books.map((b) => (
19 |
20 | {b.title}
21 | {b.author.name}
22 | {b.published}
23 |
24 | ))}
25 |
26 |
27 | );
28 | };
29 |
30 | BooksTable.propTypes = {
31 | books: PropTypes.arrayOf(PropTypes.object),
32 | };
33 |
34 | export default BooksTable;
35 |
--------------------------------------------------------------------------------
/part8/library-frontend/src/components/Login.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Helmet } from "react-helmet-async";
3 | import Container from "react-bootstrap/Container";
4 | import Row from "react-bootstrap/Row";
5 | import Col from "react-bootstrap/Col";
6 | import Card from "react-bootstrap/Card";
7 |
8 | import Notifications from "./Notifications";
9 | import LoginForm from "./LoginForm";
10 |
11 | const Login = () => {
12 | return (
13 | <>
14 |
15 | GraphQL Library | Login
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | Login
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | >
35 | );
36 | };
37 |
38 | export default Login;
39 |
--------------------------------------------------------------------------------
/part8/library-frontend/src/components/Modal.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import { Modal as StyledModal, ModalContent } from "./StyledComponents";
3 |
4 | const Modal = ({ children }) => {
5 | useEffect(() => {
6 | document.body.style.overflow = "hidden";
7 | return () => {
8 | document.body.style.overflow = "";
9 | };
10 | }, []);
11 |
12 | return (
13 |
14 | {children}
15 |
16 | );
17 | };
18 |
19 | export default Modal;
20 |
--------------------------------------------------------------------------------
/part8/library-frontend/src/components/ModalSpinner.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import Modal from "./Modal";
4 | import { LoadingSpinner } from "./StyledComponents";
5 |
6 | const ModalSpinner = () => {
7 | return (
8 |
9 |
15 | Loading...
16 |
17 |
18 | );
19 | };
20 |
21 | export default ModalSpinner;
22 |
--------------------------------------------------------------------------------
/part8/library-frontend/src/components/NoResource.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import { Link } from "react-router-dom";
4 | import Jumbotron from "react-bootstrap/Jumbotron";
5 | import Button from "react-bootstrap/Button";
6 |
7 | import useAuthUser from "../hooks/useAuthUser";
8 |
9 | const NoResource = ({ resource }) => {
10 | const { user, hasSyncAuth } = useAuthUser();
11 |
12 | if (!hasSyncAuth) return null;
13 | return (
14 |
15 | There are currently no {resource} to display.
16 |
17 | {user && (
18 |
19 | Add a new Book
20 |
21 | )}
22 | {!user && (
23 |
31 | Login to add a new Book
32 |
33 | )}
34 |
35 |
36 | );
37 | };
38 |
39 | NoResource.propTypes = {
40 | resource: PropTypes.string.isRequired,
41 | };
42 |
43 | export default NoResource;
44 |
--------------------------------------------------------------------------------
/part8/library-frontend/src/components/Notifications.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { useQuery } from "@apollo/client";
3 |
4 | import { ALL_NOTIFICATIONS } from "../graphql/queries";
5 | import limitNotifications from "../utils/limitArrayOfObjectsByDate";
6 | import Notification from "./Notification";
7 |
8 | const Notifications = () => {
9 | const [notifications, setNotifications] = useState([]);
10 |
11 | const getAllNotifications = useQuery(ALL_NOTIFICATIONS);
12 |
13 | useEffect(() => {
14 | if (getAllNotifications.data) {
15 | const notifications = getAllNotifications.data.allNotifications;
16 | setNotifications(notifications);
17 | }
18 | }, [getAllNotifications.data]);
19 |
20 | const notificationsToShow = limitNotifications(notifications, 3);
21 |
22 | return (
23 |
34 | {notificationsToShow.map((n) => (
35 |
42 | ))}
43 |
44 | );
45 | };
46 |
47 | export default Notifications;
48 |
--------------------------------------------------------------------------------
/part8/library-frontend/src/components/StyledComponents.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 | import Spinner from "react-bootstrap/Spinner";
3 |
4 | export const Modal = styled.div`
5 | display: flex;
6 | justify-content: center;
7 | align-items: center;
8 | position: absolute;
9 | top: 0;
10 | left: 0;
11 | width: 100%;
12 | height: 100%;
13 | background-color: rgba(50, 50, 50, 0.95);
14 | z-index: 9999;
15 | `;
16 |
17 | export const ModalContent = styled.div`
18 | display: flex;
19 | justify-content: center;
20 | align-items: center;
21 | `;
22 |
23 | export const LoadingSpinner = styled(Spinner)`
24 | width: 4rem;
25 | height: 4rem;
26 | font-size: 2rem;
27 |
28 | @media (min-width: 768px) {
29 | width: 6rem;
30 | height: 6rem;
31 | }
32 | `;
33 |
--------------------------------------------------------------------------------
/part8/library-frontend/src/helpers/errorHelper.js:
--------------------------------------------------------------------------------
1 | import logger from "../utils/logger";
2 |
3 | export const resolveApolloErrors = ({ graphQLErrors, networkError }) => {
4 | let errors;
5 |
6 | if (networkError) {
7 | logger.error(`[Network error]: ${networkError}`);
8 | errors = ["Something went wrong..."];
9 | } else if (graphQLErrors) {
10 | errors = graphQLErrors.reduce((result, error) => {
11 | const { message, locations, path, extensions } = error;
12 | logger.error(
13 | `[GraphQL error]: Message: ${message}, Location: ${JSON.stringify(
14 | locations
15 | )}, Path: ${path}`
16 | );
17 |
18 | const { code, errorMessages } = extensions;
19 |
20 | switch (code) {
21 | case "BAD_USER_INPUT":
22 | return result.concat(errorMessages);
23 | case "NOT_FOUND":
24 | result.push(message);
25 | return result;
26 | default:
27 | result.push(message);
28 | return result;
29 | }
30 | }, []);
31 | }
32 |
33 | return errors;
34 | };
35 |
--------------------------------------------------------------------------------
/part8/library-frontend/src/hooks/useAuthUser.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import { useQuery } from "@apollo/client";
3 |
4 | import { GET_AUTH_USER } from "../graphql/queries";
5 |
6 | const useAuthUser = () => {
7 | const [authStatus, setAuthStatus] = useState({
8 | user: null,
9 | hasSyncAuth: false,
10 | });
11 | const getAuthUser = useQuery(GET_AUTH_USER);
12 |
13 | useEffect(() => {
14 | const { called, networkStatus, data } = getAuthUser;
15 | if (called && networkStatus > 6) {
16 | const authUser = data ? data.me : null;
17 | setAuthStatus({ user: authUser, hasSyncAuth: true });
18 | }
19 | }, [getAuthUser]);
20 |
21 | return authStatus;
22 | };
23 |
24 | export default useAuthUser;
25 |
--------------------------------------------------------------------------------
/part8/library-frontend/src/hooks/useBookGenres.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import { useQuery } from "@apollo/client";
3 |
4 | import { GET_ALL_BOOKS } from "../graphql/queries";
5 |
6 | const toGenres = (genres, book) => {
7 | const newGenres = book.genres.filter((g) => g && !genres.includes(g));
8 | return genres.concat(newGenres);
9 | };
10 |
11 | const useBookGenres = () => {
12 | const [books, setBooks] = useState([]);
13 | const [genresState, setGenresState] = useState({
14 | genres: [],
15 | hasGenres: false,
16 | });
17 | const getAllBooks = useQuery(GET_ALL_BOOKS);
18 |
19 | useEffect(() => {
20 | const { called, networkStatus, data } = getAllBooks;
21 | if (called && networkStatus > 6) {
22 | const newBooks = data ? data.allBooks : books;
23 | const genres = newBooks.reduce(toGenres, []);
24 | setGenresState({ genres, hasGenres: true });
25 | setBooks(newBooks);
26 | }
27 | }, [books, getAllBooks]);
28 |
29 | return genresState;
30 | };
31 |
32 | export default useBookGenres;
33 |
--------------------------------------------------------------------------------
/part8/library-frontend/src/hooks/useYupValidationResolver.js:
--------------------------------------------------------------------------------
1 | import { useCallback } from "react";
2 |
3 | const useYupValidationResolver = (validationSchema) =>
4 | useCallback(
5 | async (data) => {
6 | try {
7 | const values = await validationSchema.validate(data, {
8 | abortEarly: false,
9 | });
10 | return {
11 | values,
12 | errors: {},
13 | };
14 | } catch (errors) {
15 | return {
16 | values: {},
17 | errors: errors.inner.reduce(
18 | (allErrors, currentError) => ({
19 | ...allErrors,
20 | [currentError.path]: {
21 | type: currentError.type ?? "validation",
22 | message: currentError.message,
23 | },
24 | }),
25 | {}
26 | ),
27 | };
28 | }
29 | },
30 | [validationSchema]
31 | );
32 |
33 | export default useYupValidationResolver;
34 |
--------------------------------------------------------------------------------
/part8/library-frontend/src/utils/limitArrayOfObjectsByDate.js:
--------------------------------------------------------------------------------
1 | const limitArrayOfObjectsByDate = (array, limit = 3, ascending = false) => {
2 | if (limit < 1) throw new Error("Invalid Limit");
3 | if (array.some((i) => !i.date)) throw new Error("No Date Property On Items");
4 |
5 | if (array.length <= limit) {
6 | return array;
7 | } else {
8 | const arrayByDateDesc = [...array].sort((a, b) => b.date - a.date);
9 |
10 | if (ascending) {
11 | const lastThreeItemsAsc = arrayByDateDesc.slice(0, limit);
12 | return lastThreeItemsAsc;
13 | } else {
14 | const lastThreeItemsDesc = arrayByDateDesc.slice(0, limit).reverse();
15 | return lastThreeItemsDesc;
16 | }
17 | }
18 | };
19 |
20 | export default limitArrayOfObjectsByDate;
21 |
--------------------------------------------------------------------------------
/part8/library-frontend/src/utils/logger.js:
--------------------------------------------------------------------------------
1 | const logger = {
2 | info: (...args) => {
3 | if (process.env.NODE_ENV === "development") {
4 | console.log(...args);
5 | }
6 | },
7 | error: (...args) => {
8 | console.error(...args);
9 | },
10 | };
11 |
12 | export default logger;
13 |
--------------------------------------------------------------------------------
/part9/patientor-backend/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | build
--------------------------------------------------------------------------------
/part9/patientor-backend/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "eslint:recommended",
4 | "plugin:@typescript-eslint/recommended",
5 | "plugin:@typescript-eslint/recommended-requiring-type-checking"
6 | ],
7 | "plugins": ["@typescript-eslint"],
8 | "env": {
9 | "browser": true,
10 | "es6": true
11 | },
12 | "rules": {
13 | "@typescript-eslint/semi": ["error"],
14 | "@typescript-eslint/explicit-function-return-type": 0,
15 | "@typescript-eslint/no-unused-vars": [
16 | "error",
17 | { "argsIgnorePattern": "^_" }
18 | ],
19 | "@typescript-eslint/no-explicit-any": 1,
20 | "no-case-declarations": 0
21 | },
22 | "parser": "@typescript-eslint/parser",
23 | "parserOptions": {
24 | "project": "./tsconfig.json"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/part9/patientor-backend/README.md:
--------------------------------------------------------------------------------
1 | # Full Stack Open 2020 - Exercise Solutions - Patientor Backend
2 |
3 | ## Part 9 - [Typescript](https://fullstackopen.com/en/part9)
4 |
5 | ## Routes
6 |
7 | The server supports the following routes
8 |
9 | - /api/ping - test route (GET)
10 | - /api/diagnoses - for diagnosis resources (GET)
11 | - /api/patients/ - for patient resources (GET, POST)
12 | - /api/patients/:id - for patient members (GET)
13 | - /api/patients/:id/entries - for entries of a patient (POST)
14 |
15 | ## usage
16 |
17 | Run application in dev watch mode (ts-node-dev) with _npm run dev_
18 |
19 | Build js files with _npm run tsc_
20 |
21 | Rebuild (deleting all old files) with _npm run tsc:rebuild_
22 |
23 | Built JavsScript src is emmited to /build folder
24 |
25 | Start application (after build) with _npm start_
26 |
27 | Run eslint with _npm run lint_
28 |
--------------------------------------------------------------------------------
/part9/patientor-backend/build/src/index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var __importDefault = (this && this.__importDefault) || function (mod) {
3 | return (mod && mod.__esModule) ? mod : { "default": mod };
4 | };
5 | Object.defineProperty(exports, "__esModule", { value: true });
6 | const express_1 = __importDefault(require("express"));
7 | const cors_1 = __importDefault(require("cors"));
8 | const diagnoses_1 = __importDefault(require("./routes/diagnoses"));
9 | const patients_1 = __importDefault(require("./routes/patients"));
10 | const app = express_1.default();
11 | app.use(express_1.default.json());
12 | app.use(cors_1.default());
13 | app.get("/api/ping", (_req, res) => {
14 | res.send("pong");
15 | });
16 | app.use("/api/diagnoses", diagnoses_1.default);
17 | app.use("/api/patients", patients_1.default);
18 | const PORT = 3001;
19 | app.listen(PORT, () => {
20 | console.log(`Server running on port ${PORT}`);
21 | });
22 |
--------------------------------------------------------------------------------
/part9/patientor-backend/build/src/routes/diagnoses.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var __importDefault = (this && this.__importDefault) || function (mod) {
3 | return (mod && mod.__esModule) ? mod : { "default": mod };
4 | };
5 | Object.defineProperty(exports, "__esModule", { value: true });
6 | const express_1 = require("express");
7 | const diagnosisService_1 = __importDefault(require("../services/diagnosisService"));
8 | const router = express_1.Router();
9 | router.get("/", (_req, res) => {
10 | res.json(diagnosisService_1.default.getDiagnoses());
11 | });
12 | exports.default = router;
13 |
--------------------------------------------------------------------------------
/part9/patientor-backend/build/src/services/diagnosisService.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var __importDefault = (this && this.__importDefault) || function (mod) {
3 | return (mod && mod.__esModule) ? mod : { "default": mod };
4 | };
5 | Object.defineProperty(exports, "__esModule", { value: true });
6 | const diagnoses_1 = __importDefault(require("../../data/diagnoses"));
7 | const getDiagnoses = () => {
8 | return diagnoses_1.default;
9 | };
10 | exports.default = {
11 | getDiagnoses,
12 | };
13 |
--------------------------------------------------------------------------------
/part9/patientor-backend/build/src/types.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | Object.defineProperty(exports, "__esModule", { value: true });
3 | var Gender;
4 | (function (Gender) {
5 | Gender["Male"] = "male";
6 | Gender["Female"] = "female";
7 | Gender["Other"] = "other";
8 | })(Gender = exports.Gender || (exports.Gender = {}));
9 | var EntryType;
10 | (function (EntryType) {
11 | EntryType["HealthCheck"] = "HealthCheck";
12 | EntryType["OccupationalHealthCare"] = "OccupationalHealthcare";
13 | EntryType["Hospital"] = "Hospital";
14 | })(EntryType = exports.EntryType || (exports.EntryType = {}));
15 | var HealthCheckRating;
16 | (function (HealthCheckRating) {
17 | HealthCheckRating[HealthCheckRating["Healthy"] = 0] = "Healthy";
18 | HealthCheckRating[HealthCheckRating["LowRisk"] = 1] = "LowRisk";
19 | HealthCheckRating[HealthCheckRating["HighRisk"] = 2] = "HighRisk";
20 | HealthCheckRating[HealthCheckRating["CriticalRisk"] = 3] = "CriticalRisk";
21 | })(HealthCheckRating = exports.HealthCheckRating || (exports.HealthCheckRating = {}));
22 |
--------------------------------------------------------------------------------
/part9/patientor-backend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "patientor-backend",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "src/index.ts",
6 | "scripts": {
7 | "tsc": "tsc",
8 | "tsc:rebuild": "del-cli ./build && npm run tsc",
9 | "start": "node build/src/index.js",
10 | "dev": "ts-node-dev src/index.ts",
11 | "lint": "eslint --ext .ts .",
12 | "test": "echo \"Error: no test specified\" && exit 1"
13 | },
14 | "devDependencies": {
15 | "@types/cors": "^2.8.6",
16 | "@types/express": "^4.17.3",
17 | "@types/uuid": "^7.0.2",
18 | "@typescript-eslint/eslint-plugin": "^2.26.0",
19 | "@typescript-eslint/parser": "^2.26.0",
20 | "del-cli": "^3.0.0",
21 | "eslint": "^6.8.0",
22 | "ts-node-dev": "^1.0.0-pre.44",
23 | "typescript": "^3.8.3"
24 | },
25 | "dependencies": {
26 | "cors": "^2.8.5",
27 | "express": "^4.17.1",
28 | "uuid": "^7.0.3"
29 | },
30 | "keywords": [],
31 | "author": "Jeremy Ebinum",
32 | "license": "MIT"
33 | }
--------------------------------------------------------------------------------
/part9/patientor-backend/src/index.ts:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import cors from "cors";
3 | import diagnosisRouter from "./routes/diagnoses";
4 | import patientRouter from "./routes/patients";
5 |
6 | const app = express();
7 |
8 | app.use(express.json());
9 | app.use(cors());
10 |
11 | app.get("/api/ping", (_req, res) => {
12 | res.send("pong");
13 | });
14 |
15 | app.use("/api/diagnoses", diagnosisRouter);
16 |
17 | app.use("/api/patients", patientRouter);
18 |
19 | const PORT = 3001;
20 |
21 | app.listen(PORT, () => {
22 | console.log(`Server running on port ${PORT}`);
23 | });
24 |
--------------------------------------------------------------------------------
/part9/patientor-backend/src/routes/diagnoses.ts:
--------------------------------------------------------------------------------
1 | import { Router } from "express";
2 | import diagnosisService from "../services/diagnosisService";
3 |
4 | const router = Router();
5 |
6 | router.get("/", (_req, res) => {
7 | res.json(diagnosisService.getDiagnoses());
8 | });
9 |
10 | export default router;
11 |
--------------------------------------------------------------------------------
/part9/patientor-backend/src/routes/patients.ts:
--------------------------------------------------------------------------------
1 | import { Router } from "express";
2 | import patientService from "../services/patientService";
3 | import { toNewPatient, toNewEntry } from "../utils";
4 |
5 | const router = Router();
6 |
7 | router.get("/", (_req, res) => {
8 | res.json(patientService.getPatients());
9 | });
10 |
11 | router.get("/:id", (req, res) => {
12 | const patient = patientService.findById(req.params.id);
13 |
14 | if (patient) {
15 | res.json(patient);
16 | } else {
17 | res.sendStatus(404);
18 | }
19 | });
20 |
21 | router.post("/", (req, res) => {
22 | try {
23 | const newPatient = toNewPatient(req.body);
24 | const addedPatient = patientService.addPatient(newPatient);
25 | res.json(addedPatient);
26 | } catch (e) {
27 | res.status(400).send({ error: e.message });
28 | }
29 | });
30 |
31 | router.post("/:id/entries", (req, res) => {
32 | const patient = patientService.findById(req.params.id);
33 |
34 | if (patient) {
35 | try {
36 | const newEntry = toNewEntry(req.body);
37 | const updatedPatient = patientService.addEntry(patient, newEntry);
38 | res.json(updatedPatient);
39 | } catch (e) {
40 | res.status(400).send({ error: e.message });
41 | }
42 | } else {
43 | res.status(404).send({ error: "Sorry, this patient does not exist" });
44 | }
45 | });
46 |
47 | export default router;
48 |
--------------------------------------------------------------------------------
/part9/patientor-backend/src/services/diagnosisService.ts:
--------------------------------------------------------------------------------
1 | import diagnoses from "../../data/diagnoses";
2 | import { Diagnosis } from "../types";
3 |
4 | const getDiagnoses = (): Diagnosis[] => {
5 | return diagnoses;
6 | };
7 |
8 | export default {
9 | getDiagnoses,
10 | };
11 |
--------------------------------------------------------------------------------
/part9/patientor-backend/src/services/patientService.ts:
--------------------------------------------------------------------------------
1 | import patients from "../../data/patients";
2 | import {
3 | Patient,
4 | NonSensitivePatient,
5 | NewPatient,
6 | Entry,
7 | NewEntry,
8 | } from "../types";
9 | import { v4 as uuid } from "uuid";
10 |
11 | let savedPatients = [...patients];
12 |
13 | /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
14 | const findById = (id: any): Patient | undefined => {
15 | const patient = savedPatients.find((p) => p.id === id);
16 | return patient;
17 | };
18 |
19 | const getPatients = (): NonSensitivePatient[] => {
20 | return savedPatients.map(
21 | ({ id, name, dateOfBirth, gender, occupation, entries }) => {
22 | return { id, name, dateOfBirth, gender, occupation, entries };
23 | }
24 | );
25 | };
26 |
27 | const addPatient = (patient: NewPatient): Patient => {
28 | const newPatient = { ...patient, id: uuid(), entries: [] as Entry[] };
29 | savedPatients = savedPatients.concat(newPatient);
30 | return newPatient;
31 | };
32 |
33 | const addEntry = (patient: Patient, newEntry: NewEntry): Patient => {
34 | const entry: Entry = { ...newEntry, id: uuid() };
35 | const savedPatient = { ...patient, entries: patient.entries.concat(entry) };
36 | savedPatients = savedPatients.map((p) =>
37 | p.id === savedPatient.id ? savedPatient : p
38 | );
39 |
40 | return savedPatient;
41 | };
42 |
43 | export default { getPatients, addPatient, findById, addEntry };
44 |
--------------------------------------------------------------------------------
/part9/patientor-backend/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES6",
4 | "outDir": "./build/",
5 | "module": "commonjs",
6 | "strict": true,
7 | "noUnusedLocals": true,
8 | "noUnusedParameters": true,
9 | "noImplicitReturns": true,
10 | "noFallthroughCasesInSwitch": true,
11 | "esModuleInterop": true,
12 | "resolveJsonModule": true
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/part9/patientor-frontend/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "eslint:recommended",
4 | "plugin:@typescript-eslint/recommended",
5 | "plugin:react/recommended",
6 | "plugin:@typescript-eslint/recommended-requiring-type-checking"
7 | ],
8 | "plugins": ["@typescript-eslint", "react"],
9 | "env": {
10 | "browser": true,
11 | "es6": true
12 | },
13 | "rules": {
14 | "@typescript-eslint/semi": ["error"],
15 | "@typescript-eslint/explicit-function-return-type": 0,
16 | "@typescript-eslint/no-unused-vars": [
17 | "error", { "argsIgnorePattern": "^_" }
18 | ],
19 | "@typescript-eslint/no-explicit-any": 1,
20 | "no-case-declarations": 0,
21 | "react/prop-types": 0
22 | },
23 | "settings": {
24 | "react": {
25 | "pragma": "React",
26 | "version": "detect"
27 | }
28 | },
29 | "parser": "@typescript-eslint/parser",
30 | "parserOptions": {
31 | "project": "./tsconfig.json"
32 | }
33 | }
--------------------------------------------------------------------------------
/part9/patientor-frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "patientor",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "axios": "^0.19.0",
7 | "formik": "^2.0.6",
8 | "react": "^16.12.0",
9 | "react-dom": "^16.12.0",
10 | "react-router-dom": "^5.1.2",
11 | "react-scripts": "3.4.0",
12 | "react-uid": "^2.2.0",
13 | "semantic-ui-css": "^2.4.1",
14 | "semantic-ui-react": "^0.88.1",
15 | "yup": "^0.28.3"
16 | },
17 | "devDependencies": {
18 | "@types/jest": "24.0.19",
19 | "@types/node": "12.11.7",
20 | "@types/react": "^16.9.11",
21 | "@types/react-dom": "16.9.3",
22 | "@types/react-router-dom": "^5.1.2",
23 | "@types/yup": "^0.26.34",
24 | "@typescript-eslint/eslint-plugin": "^2.12.0",
25 | "@typescript-eslint/parser": "^2.12.0",
26 | "eslint-config-react": "^1.1.7",
27 | "typescript": "^3.7.0"
28 | },
29 | "scripts": {
30 | "start": "react-scripts start",
31 | "build": "react-scripts build",
32 | "test": "react-scripts test",
33 | "eject": "react-scripts eject",
34 | "lint": "eslint './src/**/*.{ts,tsx}'",
35 | "lint:fix": "eslint './src/**/*.{ts,tsx}' --fix"
36 | },
37 | "browserslist": {
38 | "production": [
39 | ">0.2%",
40 | "not dead",
41 | "not op_mini all"
42 | ],
43 | "development": [
44 | "last 1 chrome version",
45 | "last 1 firefox version",
46 | "last 1 safari version"
47 | ]
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/part9/patientor-frontend/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeremy-ebinum/full-stack-open-2020/ce8ebbefb5f60c71362b12849129167348891680/part9/patientor-frontend/public/favicon.ico
--------------------------------------------------------------------------------
/part9/patientor-frontend/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "start_url": ".",
5 | "display": "standalone",
6 | "theme_color": "#000000",
7 | "background_color": "#ffffff"
8 | }
9 |
--------------------------------------------------------------------------------
/part9/patientor-frontend/src/AddEntryModal/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Modal, Segment } from "semantic-ui-react";
3 | import { NewEntry } from "../types";
4 |
5 | import AddEntryFormWrapper from "./AddEntryFormWrapper";
6 |
7 | interface Props {
8 | modalOpen: boolean;
9 | onClose: () => void;
10 | onSubmit: (values: NewEntry) => void;
11 | error?: string;
12 | }
13 |
14 | const AddEntryModal = ({ modalOpen, onClose, onSubmit, error }: Props) => (
15 |
16 | Add a new entry
17 |
18 | {error && {`${error}`} }
19 |
20 |
21 |
22 | );
23 |
24 | export default AddEntryModal;
25 |
--------------------------------------------------------------------------------
/part9/patientor-frontend/src/AddPatientModal/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Modal, Segment } from "semantic-ui-react";
3 | import AddPatientForm, { PatientFormValues } from "./AddPatientForm";
4 |
5 | interface Props {
6 | modalOpen: boolean;
7 | onClose: () => void;
8 | onSubmit: (values: PatientFormValues) => void;
9 | error?: string;
10 | }
11 |
12 | const AddPatientModal = ({ modalOpen, onClose, onSubmit, error }: Props) => (
13 |
14 | Add a new patient
15 |
16 | {error && {`${error}`} }
17 |
18 |
19 |
20 | );
21 |
22 | export default AddPatientModal;
23 |
--------------------------------------------------------------------------------
/part9/patientor-frontend/src/PatientPage/DiagnosisList.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { uid } from "react-uid";
3 |
4 | import { useStateValue } from "../state";
5 | import { Diagnosis } from "../types";
6 | import { List } from "semantic-ui-react";
7 |
8 | interface DiagnosesDetailsProps {
9 | diagnosesCodes: Array;
10 | }
11 | const DiagnosisList: React.FC = ({ diagnosesCodes }) => {
12 | const [{ diagnoses }] = useStateValue();
13 |
14 | return (
15 |
16 |
17 |
18 | {diagnosesCodes.length > 1 ? "Diagnoses" : "Diagnosis"}
19 |
20 |
21 | {diagnosesCodes.map((code) => (
22 |
23 |
24 |
25 | {code} -
26 | {diagnoses[code] && diagnoses[code].name}
27 |
28 |
29 |
30 | ))}
31 |
32 | );
33 | };
34 |
35 | export default DiagnosisList;
36 |
--------------------------------------------------------------------------------
/part9/patientor-frontend/src/PatientPage/EntryDetails.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { Entry, EntryType } from "../types";
4 | import { assertNever } from "../utils";
5 |
6 | import HealthCheckEntry from "./HealthCheckEntry";
7 | import OccupationalHealthCareEntry from "./OccupationalHealthCareEntry";
8 | import HospitalEntry from "./HospitalEntry";
9 |
10 | const EntryDetails: React.FC<{ entry: Entry }> = ({ entry }) => {
11 | switch (entry.type) {
12 | case EntryType.HealthCheck:
13 | return ;
14 | case EntryType.OccupationalHealthCare:
15 | return ;
16 | case EntryType.Hospital:
17 | return ;
18 | default:
19 | return assertNever(entry);
20 | }
21 | };
22 |
23 | export default EntryDetails;
24 |
--------------------------------------------------------------------------------
/part9/patientor-frontend/src/PatientPage/HealthCheckEntry.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Card, Icon } from "semantic-ui-react";
3 |
4 | import { HealthCheckEntry as HealthCheck } from "../types";
5 |
6 | import HealthRatingBar from "../components/HealthRatingBar";
7 | import DiagnosisList from "./DiagnosisList";
8 |
9 | const HealthCheckEntry: React.FC<{ entry: HealthCheck }> = ({ entry }) => {
10 | return (
11 |
12 |
13 |
14 | {entry.date}
15 |
16 | by {entry.specialist}
17 | {entry.description}
18 | {entry.diagnosisCodes && (
19 |
20 | )}
21 |
22 |
23 |
24 |
25 |
26 | );
27 | };
28 |
29 | export default HealthCheckEntry;
30 |
--------------------------------------------------------------------------------
/part9/patientor-frontend/src/PatientPage/HospitalEntry.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Card, Icon, List } from "semantic-ui-react";
3 |
4 | import { HospitalEntry as Hospital } from "../types";
5 |
6 | import DiagnosisList from "./DiagnosisList";
7 |
8 | const HospitalEntry: React.FC<{ entry: Hospital }> = ({ entry }) => {
9 | return (
10 |
11 |
12 |
13 | {entry.date}
14 |
15 | by {entry.specialist}
16 | {entry.description}
17 | {entry.diagnosisCodes && (
18 |
19 | )}
20 |
21 |
22 |
23 |
24 |
25 | Discharged on {entry.discharge.date}
26 | {entry.discharge.criteria}
27 |
28 |
29 |
30 |
31 | );
32 | };
33 |
34 | export default HospitalEntry;
35 |
--------------------------------------------------------------------------------
/part9/patientor-frontend/src/PatientPage/OccupationalHealthCareEntry.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Card, Icon, List } from "semantic-ui-react";
3 |
4 | import { OccupationalHealthCareEntry as OccupationalHealthCare } from "../types";
5 |
6 | import DiagnosisList from "./DiagnosisList";
7 |
8 | const OccupationalHealthCareEntry: React.FC<{
9 | entry: OccupationalHealthCare;
10 | }> = ({ entry }) => {
11 | return (
12 |
13 |
14 |
15 | {entry.date}
16 |
17 | by {entry.specialist}
18 | {entry.description}
19 | {entry.diagnosisCodes && (
20 |
21 | )}
22 |
23 |
24 |
25 | Employer: {entry.employerName}
26 |
27 | {entry.sickLeave && (
28 |
29 | Sick Leave: {entry.sickLeave.startDate} to{" "}
30 | {entry.sickLeave.endDate}
31 |
32 | )}
33 |
34 |
35 |
36 | );
37 | };
38 |
39 | export default OccupationalHealthCareEntry;
40 |
--------------------------------------------------------------------------------
/part9/patientor-frontend/src/components/HealthRatingBar.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Rating } from 'semantic-ui-react';
3 |
4 | type BarProps = {
5 | rating: number;
6 | showText: boolean;
7 | };
8 |
9 | const HEALTHBAR_TEXTS = [
10 | 'The patient is in great shape',
11 | 'The patient has a low risk of getting sick',
12 | 'The patient has a high risk of getting sick',
13 | 'The patient has a diagnosed condition',
14 | ];
15 |
16 | const HealthRatingBar = ({ rating, showText }: BarProps) => {
17 | return (
18 |
19 | {
}
20 | {showText ?
{HEALTHBAR_TEXTS[rating]}
: null}
21 |
22 | );
23 | };
24 |
25 | export default HealthRatingBar;
26 |
--------------------------------------------------------------------------------
/part9/patientor-frontend/src/constants.ts:
--------------------------------------------------------------------------------
1 | export const apiBaseUrl = 'http://localhost:3001/api';
2 |
--------------------------------------------------------------------------------
/part9/patientor-frontend/src/helpers/errorHelper.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-explicit-any */
2 | export class InvalidPatientError extends Error {
3 | /* eslint-disable-next-line */
4 | constructor(...params: any) {
5 | // Pass remaining arguments (including vendor specific ones) to parent constructor
6 | super(...params);
7 |
8 | // Maintains proper stack trace for where our error was thrown (only available on V8)
9 | if (Error.captureStackTrace) {
10 | Error.captureStackTrace(this, InvalidPatientError);
11 | }
12 |
13 | this.name = "InvalidPatientError";
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/part9/patientor-frontend/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import 'semantic-ui-css/semantic.min.css';
4 | import App from './App';
5 | import { reducer, StateProvider } from "./state";
6 |
7 | ReactDOM.render(
8 |
9 |
10 | ,
11 | document.getElementById('root')
12 | );
13 |
--------------------------------------------------------------------------------
/part9/patientor-frontend/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/part9/patientor-frontend/src/state/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./reducer";
2 | export * from "./state";
3 |
--------------------------------------------------------------------------------
/part9/patientor-frontend/src/state/state.tsx:
--------------------------------------------------------------------------------
1 | import React, { createContext, useContext, useReducer } from "react";
2 | import { Patient, Diagnosis } from "../types";
3 |
4 | import { Action } from "./reducer";
5 |
6 | export type State = {
7 | patients: { [id: string]: Patient };
8 | diagnoses: { [code: string]: Diagnosis };
9 | };
10 |
11 | const initialState: State = {
12 | patients: {},
13 | diagnoses: {},
14 | };
15 |
16 | export const StateContext = createContext<[State, React.Dispatch]>([
17 | initialState,
18 | () => initialState,
19 | ]);
20 |
21 | type StateProviderProps = {
22 | reducer: React.Reducer;
23 | children: React.ReactElement;
24 | };
25 |
26 | export const StateProvider: React.FC = ({
27 | reducer,
28 | children,
29 | }: StateProviderProps) => {
30 | const [state, dispatch] = useReducer(reducer, initialState);
31 | return (
32 |
33 | {children}
34 |
35 | );
36 | };
37 | export const useStateValue = () => useContext(StateContext);
38 |
--------------------------------------------------------------------------------
/part9/patientor-frontend/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "skipLibCheck": true,
10 | "esModuleInterop": true,
11 | "allowSyntheticDefaultImports": true,
12 | "strict": true,
13 | "forceConsistentCasingInFileNames": true,
14 | "module": "esnext",
15 | "moduleResolution": "node",
16 | "resolveJsonModule": true,
17 | "isolatedModules": true,
18 | "noEmit": true,
19 | "jsx": "react",
20 | "downlevelIteration": true,
21 | "allowJs": true
22 | },
23 | "include": [
24 | "src"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------
/part9/ts-first-steps/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "eslint:recommended",
4 | "plugin:@typescript-eslint/recommended",
5 | "plugin:@typescript-eslint/recommended-requiring-type-checking"
6 | ],
7 | "plugins": ["@typescript-eslint"],
8 | "env": {
9 | "node": true,
10 | "es6": true
11 | },
12 | "rules": {
13 | "@typescript-eslint/semi": ["error"],
14 | "@typescript-eslint/no-explicit-any": 2,
15 | "@typescript-eslint/explicit-function-return-type": 0,
16 | "@typescript-eslint/no-unused-vars": [
17 | "error",
18 | { "argsIgnorePattern": "^_" }
19 | ],
20 | "no-case-declarations": 0
21 | },
22 | "parser": "@typescript-eslint/parser",
23 | "parserOptions": {
24 | "project": "./tsconfig.json"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/part9/ts-first-steps/README.md:
--------------------------------------------------------------------------------
1 | # Full Stack Open 2020 - Exercise Solutions - First Steps With Typescript
2 |
3 | ## Part 9 - [Typescript](https://fullstackopen.com/en/part9)
4 |
5 | ## usage
6 |
7 | Start the express server with _npm start_
8 |
9 | Start the server in watch mode with _npm run dev_
10 |
11 | Go to /hello for routes information
12 |
13 | Run bmi calculator from cmd line with _npm run calculateBmi height(cm) weight(kg)_
14 |
15 | Run exercise calculator from cmd line with _npm run calculateExercises target(hrs) dailyHours(hrs)[]_
16 |
--------------------------------------------------------------------------------
/part9/ts-first-steps/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ts-first-steps",
3 | "version": "1.0.0",
4 | "main": "index.js",
5 | "dependencies": {
6 | "body-parser": "^1.19.0",
7 | "cors": "^2.8.5",
8 | "express": "^4.17.1"
9 | },
10 | "devDependencies": {
11 | "@types/body-parser": "^1.19.0",
12 | "@types/cors": "^2.8.6",
13 | "@types/express": "^4.17.3",
14 | "@types/node": "^13.9.5",
15 | "@typescript-eslint/eslint-plugin": "^2.26.0",
16 | "@typescript-eslint/parser": "^2.26.0",
17 | "eslint": "^6.8.0",
18 | "ts-node": "^8.8.1",
19 | "ts-node-dev": "^1.0.0-pre.44",
20 | "typescript": "^3.8.3"
21 | },
22 | "scripts": {
23 | "start": "ts-node index.ts",
24 | "dev": "ts-node-dev index.ts",
25 | "calculateBmi": "ts-node calculateBmi.ts",
26 | "calculateExercises": "ts-node exerciseCalculator.ts",
27 | "lint": "eslint --ext .ts .",
28 | "test": "echo \"Error: no test specified\" && exit 1"
29 | },
30 | "keywords": [],
31 | "author": "Jeremy Ebinum",
32 | "license": "MIT",
33 | "description": ""
34 | }
35 |
--------------------------------------------------------------------------------
/part9/ts-first-steps/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "noImplicitAny": true,
4 | "noImplicitReturns": true,
5 | "strictNullChecks": true,
6 | "strictPropertyInitialization": true,
7 | "strictBindCallApply": true,
8 | "noUnusedLocals": true,
9 | "noUnusedParameters": true,
10 | "noImplicitThis": true,
11 | "alwaysStrict": true,
12 | "esModuleInterop": true,
13 | "declaration": true
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/part9/typed-courseinfo/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es6": true
5 | },
6 | "extends": [
7 | "eslint:recommended",
8 | "plugin:react/recommended",
9 | "plugin:@typescript-eslint/recommended"
10 | ],
11 | "plugins": ["react", "@typescript-eslint"],
12 | "settings": {
13 | "react": {
14 | "pragma": "React",
15 | "version": "detect"
16 | }
17 | },
18 | "rules": {
19 | "react/prop-types": 0,
20 | "@typescript-eslint/explicit-function-return-type": 0
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/part9/typed-courseinfo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "typed-courseinfo",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^4.2.4",
7 | "@testing-library/react": "^9.5.0",
8 | "@testing-library/user-event": "^7.2.1",
9 | "@types/jest": "^24.9.1",
10 | "@types/node": "^12.12.34",
11 | "@types/react": "^16.9.32",
12 | "@types/react-dom": "^16.9.6",
13 | "react": "^16.13.1",
14 | "react-dom": "^16.13.1",
15 | "react-scripts": "3.4.1",
16 | "react-uid": "^2.2.0",
17 | "typescript": "^3.7.5"
18 | },
19 | "scripts": {
20 | "start": "react-scripts start",
21 | "build": "react-scripts build",
22 | "test": "react-scripts test",
23 | "eject": "react-scripts eject",
24 | "lint": "eslint ./src/**/*.{ts,tsx}"
25 | },
26 | "eslintConfig": {
27 | "extends": "react-app"
28 | },
29 | "browserslist": {
30 | "production": [
31 | ">0.2%",
32 | "not dead",
33 | "not op_mini all"
34 | ],
35 | "development": [
36 | "last 1 chrome version",
37 | "last 1 firefox version",
38 | "last 1 safari version"
39 | ]
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/part9/typed-courseinfo/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeremy-ebinum/full-stack-open-2020/ce8ebbefb5f60c71362b12849129167348891680/part9/typed-courseinfo/public/favicon.ico
--------------------------------------------------------------------------------
/part9/typed-courseinfo/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeremy-ebinum/full-stack-open-2020/ce8ebbefb5f60c71362b12849129167348891680/part9/typed-courseinfo/public/logo192.png
--------------------------------------------------------------------------------
/part9/typed-courseinfo/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeremy-ebinum/full-stack-open-2020/ce8ebbefb5f60c71362b12849129167348891680/part9/typed-courseinfo/public/logo512.png
--------------------------------------------------------------------------------
/part9/typed-courseinfo/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/part9/typed-courseinfo/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/part9/typed-courseinfo/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { uid } from "react-uid";
3 |
4 | import { CoursePart } from "./types";
5 |
6 | import Header from "./components/Header";
7 | import Content from "./components/Content";
8 | import Total from "./components/Total";
9 |
10 | const App: React.FC = () => {
11 | const courseName = "Half Stack application development";
12 | const courseParts: CoursePart[] = [
13 | {
14 | name: "Fundamentals",
15 | exerciseCount: 10,
16 | description: "This is an awesome course part",
17 | id: uid({}),
18 | },
19 | {
20 | name: "Using props to pass data",
21 | exerciseCount: 7,
22 | groupProjectCount: 3,
23 | id: uid({}),
24 | },
25 | {
26 | name: "Deeper type usage",
27 | exerciseCount: 14,
28 | description: "Confusing description",
29 | exerciseSubmissionLink: "https://fake-exercise-submit.made-up-url.dev",
30 | id: uid({}),
31 | },
32 | {
33 | name: "A History of Spam and Eggs",
34 | exerciseCount: 1,
35 | studentCount: 7e9,
36 | description: "Quite possibly the most important course in the world",
37 | id: uid({}),
38 | },
39 | ];
40 |
41 | return (
42 |
43 |
44 |
45 |
46 |
47 | );
48 | };
49 |
50 | export default App;
51 |
--------------------------------------------------------------------------------
/part9/typed-courseinfo/src/components/Content.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { CoursePart } from "../types";
4 | import Part from "./Part";
5 |
6 | const Content: React.FC<{ courseParts: CoursePart[] }> = ({ courseParts }) => {
7 | return (
8 | <>
9 | {courseParts.map((part) => (
10 |
11 | ))}
12 | >
13 | );
14 | };
15 |
16 | export default Content;
17 |
--------------------------------------------------------------------------------
/part9/typed-courseinfo/src/components/Header.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const Header: React.FC<{ courseName: string }> = ({ courseName }) => {
4 | return {courseName} ;
5 | };
6 |
7 | export default Header;
8 |
--------------------------------------------------------------------------------
/part9/typed-courseinfo/src/components/Total.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { CoursePart } from "../types";
4 |
5 | const Total: React.FC<{ courseParts: CoursePart[] }> = ({ courseParts }) => {
6 | return (
7 |
8 | Total number of exercises:{" "}
9 |
10 | {courseParts.reduce((carry, part) => carry + part.exerciseCount, 0)}
11 |
12 |
13 | );
14 | };
15 |
16 | export default Total;
17 |
--------------------------------------------------------------------------------
/part9/typed-courseinfo/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/part9/typed-courseinfo/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import "./index.css";
4 | import App from "./App";
5 |
6 | ReactDOM.render(
7 |
8 |
9 | ,
10 | document.getElementById("root")
11 | );
12 |
--------------------------------------------------------------------------------
/part9/typed-courseinfo/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/part9/typed-courseinfo/src/setupTests.ts:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom/extend-expect';
6 |
--------------------------------------------------------------------------------
/part9/typed-courseinfo/src/types.ts:
--------------------------------------------------------------------------------
1 | export interface CoursePartBase {
2 | name: string;
3 | exerciseCount: number;
4 | id: string;
5 | }
6 |
7 | interface CoursePartBaseWithOptionalDescription extends CoursePartBase {
8 | description?: string;
9 | }
10 |
11 | interface CoursePartOne extends CoursePartBaseWithOptionalDescription {
12 | name: "Fundamentals";
13 | }
14 |
15 | interface CoursePartTwo extends CoursePartBase {
16 | name: "Using props to pass data";
17 | groupProjectCount: number;
18 | }
19 |
20 | interface CoursePartThree extends CoursePartBaseWithOptionalDescription {
21 | name: "Deeper type usage";
22 | exerciseSubmissionLink: string;
23 | }
24 |
25 | interface CoursePartFour extends CoursePartBase {
26 | name: "A History of Spam and Eggs";
27 | description: string;
28 | studentCount: number;
29 | }
30 |
31 | export type CoursePart =
32 | | CoursePartOne
33 | | CoursePartTwo
34 | | CoursePartThree
35 | | CoursePartFour;
36 |
--------------------------------------------------------------------------------
/part9/typed-courseinfo/src/utils.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Helper function for exhaustive type checking
3 | */
4 | export const assertNever = (value: never): never => {
5 | throw new Error(
6 | `Unhandled discriminated union member: ${JSON.stringify(value)}`
7 | );
8 | };
9 |
--------------------------------------------------------------------------------
/part9/typed-courseinfo/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "module": "esnext",
16 | "moduleResolution": "node",
17 | "resolveJsonModule": true,
18 | "isolatedModules": true,
19 | "noEmit": true,
20 | "jsx": "react"
21 | },
22 | "include": [
23 | "src"
24 | ]
25 | }
26 |
--------------------------------------------------------------------------------