├── .gitignore ├── README.md ├── certificate.png ├── part0 ├── 0.4_new_note.png ├── 0.5_single_page_app.png └── 0.6_new_note_spa.png ├── part1 ├── anecdotes │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ └── robots.txt │ └── src │ │ └── index.js ├── courseinfo │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ └── robots.txt │ └── src │ │ └── index.js └── unicafe │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt │ └── src │ └── index.js ├── part2 ├── countries │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ └── robots.txt │ └── src │ │ ├── App.js │ │ ├── components │ │ ├── Country.js │ │ └── CountryInfo.js │ │ └── index.js ├── courseinfo │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ └── robots.txt │ └── src │ │ ├── App.js │ │ ├── Course.js │ │ └── index.js └── phonebook │ ├── README.md │ ├── db.json │ ├── package-lock.json │ ├── package.json │ ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt │ └── src │ ├── App.js │ ├── components │ ├── FilterInput.js │ ├── NewPersonForm.js │ ├── Notification.js │ ├── Person.js │ └── Persons.js │ ├── index.css │ ├── index.js │ └── services │ └── persons.js ├── part3 └── README.md ├── part4 ├── .eslintignore ├── .eslintrc.js ├── app.js ├── controllers │ ├── blogs.js │ ├── login.js │ ├── testing.js │ └── users.js ├── index.js ├── jest.config.js ├── models │ ├── blog.js │ └── user.js ├── package-lock.json ├── package.json ├── tests │ ├── blogs_api.test.js │ ├── list_helper.test.js │ ├── test_helper.js │ └── users_api.test.js └── utils │ ├── config.js │ ├── list_helper.js │ ├── logger.js │ └── middleware.js ├── part5 ├── .eslintignore ├── .eslintrc.js ├── .prettierrc ├── cypress.json ├── cypress │ ├── fixtures │ │ └── example.json │ ├── integration │ │ └── bloglist_app.spec.js │ ├── plugins │ │ └── index.js │ └── support │ │ ├── commands.js │ │ └── index.js ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt └── src │ ├── App.js │ ├── components │ ├── Blog.js │ ├── Blog.test.js │ ├── Bloglist.js │ ├── CreateBlogForm.js │ ├── CreateBlogForm.test.js │ ├── Header.js │ ├── LoginForm.js │ ├── Notification.js │ └── Togglable.js │ ├── index.css │ ├── index.js │ ├── services │ ├── blogs.js │ └── login.js │ └── setupTests.js ├── part6 ├── redux-anecdotes │ ├── db.json │ ├── package-lock.json │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ └── robots.txt │ └── src │ │ ├── App.js │ │ ├── components │ │ ├── AnecdoteForm.js │ │ ├── AnecdoteList.js │ │ ├── Filter.js │ │ └── Notification.js │ │ ├── index.js │ │ ├── reducers │ │ ├── anecdoteReducer.js │ │ ├── filterReducer.js │ │ └── notificationReducer.js │ │ ├── services │ │ └── anecdotes.js │ │ └── store.js └── unicafe-redux │ ├── package-lock.json │ ├── package.json │ ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt │ └── src │ ├── index.js │ ├── reducer.js │ └── reducer.test.js ├── part7 ├── bloglist-backend │ ├── .eslintignore │ ├── .eslintrc.js │ ├── app.js │ ├── controllers │ │ ├── blogs.js │ │ ├── login.js │ │ ├── testing.js │ │ └── users.js │ ├── index.js │ ├── jest.config.js │ ├── models │ │ ├── blog.js │ │ └── user.js │ ├── package-lock.json │ ├── package.json │ ├── tests │ │ ├── blogs_api.test.js │ │ ├── list_helper.test.js │ │ ├── test_helper.js │ │ └── users_api.test.js │ └── utils │ │ ├── config.js │ │ ├── list_helper.js │ │ ├── logger.js │ │ └── middleware.js ├── bloglist-frontend │ ├── .eslintignore │ ├── .eslintrc.js │ ├── .prettierrc │ ├── cypress.json │ ├── package-lock.json │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ └── robots.txt │ └── src │ │ ├── App.js │ │ ├── components │ │ ├── Blog.js │ │ ├── Blogs.js │ │ ├── CreateBlogForm.js │ │ ├── LoginForm.js │ │ ├── MainHeader.js │ │ ├── Notification.js │ │ ├── Togglable.js │ │ ├── User.js │ │ └── Users.js │ │ ├── globalStyles.js │ │ ├── hooks │ │ └── index.js │ │ ├── index.css │ │ ├── index.js │ │ ├── reducers │ │ ├── blogReducer.js │ │ ├── currentUserReducer.js │ │ ├── notificationReducer.js │ │ └── userReducer.js │ │ ├── services │ │ ├── blogs.js │ │ ├── login.js │ │ └── users.js │ │ └── store.js ├── country-hook │ ├── package-lock.json │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ └── robots.txt │ └── src │ │ ├── App.js │ │ └── index.js ├── routed-anecdotes │ ├── package-lock.json │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ └── robots.txt │ └── src │ │ ├── App.js │ │ ├── hooks │ │ └── index.js │ │ └── index.js └── ultimate-hooks │ ├── db.json │ ├── package-lock.json │ ├── package.json │ ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt │ └── src │ ├── App.js │ └── index.js ├── part8 ├── library-backend │ ├── index.js │ ├── models │ │ ├── Author.js │ │ ├── Book.js │ │ └── User.js │ ├── package-lock.json │ ├── package.json │ ├── resolvers.js │ └── typeDefs.js └── library-frontend │ ├── package-lock.json │ ├── package.json │ ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt │ └── src │ ├── App.js │ ├── components │ ├── Authors.js │ ├── Books.js │ ├── LoginForm.js │ ├── NewBook.js │ ├── Notification.js │ ├── Recommend.js │ └── SetBirthYear.js │ ├── index.js │ └── queries.js └── part9 ├── courseinfo_ts ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt ├── src │ ├── App.tsx │ ├── Content.tsx │ ├── Header.tsx │ ├── Part.tsx │ ├── Total.tsx │ ├── index.tsx │ ├── react-app-env.d.ts │ ├── types.ts │ └── utils.ts └── tsconfig.json ├── first_steps_with_typescript ├── .eslintrc ├── bmiCalculator.ts ├── exerciseCalculator.ts ├── index.ts ├── package-lock.json ├── package.json └── tsconfig.json ├── patientor-backend ├── .eslintignore ├── .eslintrc ├── .gitignore ├── data │ ├── diagnoses.ts │ └── patients.ts ├── package-lock.json ├── package.json ├── src │ ├── index.ts │ ├── routes │ │ ├── diagnosesRouter.ts │ │ └── patientsRouter.ts │ ├── services │ │ ├── diagnosesService.ts │ │ └── patientsService.ts │ ├── types.ts │ └── utils.ts └── tsconfig.json └── patientor-frontend ├── .eslintrc ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html └── manifest.json ├── src ├── AddEntryModal │ ├── AddEntryForm.tsx │ ├── FormField.tsx │ └── index.tsx ├── AddPatientModal │ ├── AddPatientForm.tsx │ ├── FormField.tsx │ └── index.tsx ├── App.tsx ├── PatientListPage │ └── index.tsx ├── PatientPage │ ├── DiagnosisList.tsx │ ├── Entries.tsx │ ├── EntryDetails.tsx │ └── index.tsx ├── components │ └── HealthRatingBar.tsx ├── constants.ts ├── index.tsx ├── react-app-env.d.ts ├── state │ ├── index.ts │ ├── reducer.ts │ └── state.tsx ├── types.ts └── utils.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | **/**/node_modules 2 | **/**/build 3 | **/**/.env 4 | **/**/coverage 5 | **/**/cypress/videos 6 | **/**/cypress/screenshots 7 | 8 | **/**/**/node_modules 9 | **/**/**/build 10 | **/**/**/.env 11 | **/**/**/coverage 12 | **/**/**/cypress/videos 13 | **/**/**/cypress/screenshots 14 | -------------------------------------------------------------------------------- /certificate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orrsteinberg/fullstackopen-2020/2565b48898ac7249e645ba3ba96fdef4780b782a/certificate.png -------------------------------------------------------------------------------- /part0/0.4_new_note.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orrsteinberg/fullstackopen-2020/2565b48898ac7249e645ba3ba96fdef4780b782a/part0/0.4_new_note.png -------------------------------------------------------------------------------- /part0/0.5_single_page_app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orrsteinberg/fullstackopen-2020/2565b48898ac7249e645ba3ba96fdef4780b782a/part0/0.5_single_page_app.png -------------------------------------------------------------------------------- /part0/0.6_new_note_spa.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orrsteinberg/fullstackopen-2020/2565b48898ac7249e645ba3ba96fdef4780b782a/part0/0.6_new_note_spa.png -------------------------------------------------------------------------------- /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.5.0", 8 | "@testing-library/user-event": "^7.2.1", 9 | "react": "^16.13.0", 10 | "react-dom": "^16.13.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/orrsteinberg/fullstackopen-2020/2565b48898ac7249e645ba3ba96fdef4780b782a/part1/anecdotes/public/favicon.ico -------------------------------------------------------------------------------- /part1/anecdotes/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /part1/anecdotes/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orrsteinberg/fullstackopen-2020/2565b48898ac7249e645ba3ba96fdef4780b782a/part1/anecdotes/public/logo192.png -------------------------------------------------------------------------------- /part1/anecdotes/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orrsteinberg/fullstackopen-2020/2565b48898ac7249e645ba3ba96fdef4780b782a/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/courseinfo/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.5.0", 8 | "@testing-library/user-event": "^7.2.1", 9 | "react": "^16.13.0", 10 | "react-dom": "^16.13.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/orrsteinberg/fullstackopen-2020/2565b48898ac7249e645ba3ba96fdef4780b782a/part1/courseinfo/public/favicon.ico -------------------------------------------------------------------------------- /part1/courseinfo/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /part1/courseinfo/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orrsteinberg/fullstackopen-2020/2565b48898ac7249e645ba3ba96fdef4780b782a/part1/courseinfo/public/logo192.png -------------------------------------------------------------------------------- /part1/courseinfo/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orrsteinberg/fullstackopen-2020/2565b48898ac7249e645ba3ba96fdef4780b782a/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.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | 4 | const Header = ({ course }) =>

{course}

; 5 | 6 | const Part = ({ name, exercises }) => ( 7 |

8 | {name}: {exercises} 9 |

10 | ); 11 | 12 | const Content = ({ parts }) => { 13 | const [part1, part2, part3] = parts; 14 | return ( 15 |
16 | 17 | 18 | 19 |
20 | ); 21 | }; 22 | 23 | const Total = ({ parts }) => { 24 | const total = parts.reduce((total, part) => total + part.exercises, 0); 25 | 26 | return
Total exercises: {total}
; 27 | }; 28 | 29 | const App = () => { 30 | const course = { 31 | name: "Half Stack application development", 32 | parts: [ 33 | { 34 | name: "Fundamentals of React", 35 | exercises: 10, 36 | }, 37 | { 38 | name: "Using props to pass data", 39 | exercises: 7, 40 | }, 41 | { 42 | name: "State of a component", 43 | exercises: 14, 44 | }, 45 | ], 46 | }; 47 | 48 | return ( 49 |
50 |
51 | 52 | 53 |
54 | ); 55 | }; 56 | 57 | ReactDOM.render(, document.getElementById("root")); 58 | -------------------------------------------------------------------------------- /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.5.0", 8 | "@testing-library/user-event": "^7.2.1", 9 | "react": "^16.13.0", 10 | "react-dom": "^16.13.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/orrsteinberg/fullstackopen-2020/2565b48898ac7249e645ba3ba96fdef4780b782a/part1/unicafe/public/favicon.ico -------------------------------------------------------------------------------- /part1/unicafe/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /part1/unicafe/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orrsteinberg/fullstackopen-2020/2565b48898ac7249e645ba3ba96fdef4780b782a/part1/unicafe/public/logo192.png -------------------------------------------------------------------------------- /part1/unicafe/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orrsteinberg/fullstackopen-2020/2565b48898ac7249e645ba3ba96fdef4780b782a/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.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import ReactDOM from "react-dom"; 3 | 4 | const Statistics = ({ good, neutral, bad }) => { 5 | const total = good + neutral + bad; 6 | const average = good * 1 + neutral * 0 + bad * -1; // Good = 1, neutral = 0, bad = -1 7 | const positivePercent = `${(good / total) * 100}%`; 8 | 9 | if (good > 0 || neutral > 0 || bad > 0) { 10 | return ( 11 |
12 |

Statistics

13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 |
24 | ); 25 | } 26 | 27 | return

No feedback given

; 28 | }; 29 | 30 | const Statistic = ({ text, value }) => ( 31 | 32 | {text} 33 | {value} 34 | 35 | ); 36 | 37 | const Button = ({ handleClick, text }) => ( 38 | 39 | ); 40 | 41 | const App = () => { 42 | const [good, setGood] = useState(0); 43 | const [neutral, setNeutral] = useState(0); 44 | const [bad, setBad] = useState(0); 45 | 46 | const handleGoodVote = () => setGood(good + 1); 47 | 48 | const handleNeutralVote = () => setNeutral(neutral + 1); 49 | 50 | const handleBadVote = () => setBad(bad + 1); 51 | 52 | return ( 53 | <> 54 |

Give feedback

55 | 14 | 15 | 16 | ); 17 | } 18 | 19 | return ( 20 |
21 | {country.name}{" "} 22 | 23 |
24 | ); 25 | }; 26 | 27 | export default Country; 28 | -------------------------------------------------------------------------------- /part2/countries/src/components/CountryInfo.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const CountryInfo = ({ country }) => ( 4 |
5 |

{country.name}

6 |

Capital: {country.capital}

7 |

Population: {country.population}

8 |

Languages:

9 |
    10 | {country.languages.map((lang, i) => ( 11 |
  • {lang.name}
  • 12 | ))} 13 |
14 | {`${country.name} 15 |
16 | ); 17 | 18 | export default CountryInfo; 19 | -------------------------------------------------------------------------------- /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/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.5.0", 8 | "@testing-library/user-event": "^7.2.1", 9 | "react": "^16.13.0", 10 | "react-dom": "^16.13.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/courseinfo/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orrsteinberg/fullstackopen-2020/2565b48898ac7249e645ba3ba96fdef4780b782a/part2/courseinfo/public/favicon.ico -------------------------------------------------------------------------------- /part2/courseinfo/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /part2/courseinfo/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orrsteinberg/fullstackopen-2020/2565b48898ac7249e645ba3ba96fdef4780b782a/part2/courseinfo/public/logo192.png -------------------------------------------------------------------------------- /part2/courseinfo/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orrsteinberg/fullstackopen-2020/2565b48898ac7249e645ba3ba96fdef4780b782a/part2/courseinfo/public/logo512.png -------------------------------------------------------------------------------- /part2/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 | -------------------------------------------------------------------------------- /part2/courseinfo/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /part2/courseinfo/src/App.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import Course from "./Course"; 4 | 5 | const App = () => { 6 | const courses = [ 7 | { 8 | name: "Half Stack application development", 9 | id: 1, 10 | parts: [ 11 | { 12 | name: "Fundamentals of React", 13 | exercises: 10, 14 | id: 1, 15 | }, 16 | { 17 | name: "Using props to pass data", 18 | exercises: 7, 19 | id: 2, 20 | }, 21 | { 22 | name: "State of a component", 23 | exercises: 14, 24 | id: 3, 25 | }, 26 | { 27 | name: "Redux", 28 | exercises: 11, 29 | id: 4, 30 | }, 31 | ], 32 | }, 33 | { 34 | name: "Node.js", 35 | id: 2, 36 | parts: [ 37 | { 38 | name: "Routing", 39 | exercises: 3, 40 | id: 1, 41 | }, 42 | { 43 | name: "Middlewares", 44 | exercises: 7, 45 | id: 2, 46 | }, 47 | ], 48 | }, 49 | ]; 50 | 51 | if (courses.length > 0) { 52 | return ( 53 |
54 | {courses.map((course) => ( 55 | 56 | ))} 57 |
58 | ); 59 | } 60 | 61 | return
There are no courses to show!
; 62 | }; 63 | 64 | export default App; 65 | -------------------------------------------------------------------------------- /part2/courseinfo/src/Course.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | // Main Component 4 | 5 | const Course = ({ course }) => { 6 | return ( 7 |
8 |
9 | 10 | 11 |
12 | ); 13 | }; 14 | 15 | // Sub Components 16 | 17 | const Header = ({ name }) => { 18 | return

{name}

; 19 | }; 20 | 21 | const Total = ({ parts }) => { 22 | const sum = parts.reduce((total, part) => total + part.exercises, 0); 23 | return ( 24 |

25 | Total number of exercises: {sum} 26 |

27 | ); 28 | }; 29 | 30 | const Part = ({ part }) => { 31 | return ( 32 |

33 | {part.name}: {part.exercises} 34 |

35 | ); 36 | }; 37 | 38 | const Content = ({ parts }) => { 39 | return ( 40 |
41 | {parts.map((part) => ( 42 | 43 | ))} 44 |
45 | ); 46 | }; 47 | 48 | export default Course; 49 | -------------------------------------------------------------------------------- /part2/courseinfo/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | 4 | import App from "./App"; 5 | 6 | ReactDOM.render(, document.getElementById("root")); 7 | -------------------------------------------------------------------------------- /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.5.0", 8 | "@testing-library/user-event": "^7.2.1", 9 | "axios": "^0.19.2", 10 | "react": "^16.13.1", 11 | "react-dom": "^16.13.1", 12 | "react-scripts": "3.4.1" 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 -p3001 --watch db.json" 20 | }, 21 | "proxy": "http://localhost:3001", 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 | "devDependencies": { 38 | "json-server": "^0.16.1" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /part2/phonebook/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orrsteinberg/fullstackopen-2020/2565b48898ac7249e645ba3ba96fdef4780b782a/part2/phonebook/public/favicon.ico -------------------------------------------------------------------------------- /part2/phonebook/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | Phonebook App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /part2/phonebook/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orrsteinberg/fullstackopen-2020/2565b48898ac7249e645ba3ba96fdef4780b782a/part2/phonebook/public/logo192.png -------------------------------------------------------------------------------- /part2/phonebook/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orrsteinberg/fullstackopen-2020/2565b48898ac7249e645ba3ba96fdef4780b782a/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/src/components/FilterInput.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const FilterInput = ({ setFilter }) => ( 4 | setFilter(target.value)} /> 5 | ); 6 | 7 | export default FilterInput; 8 | -------------------------------------------------------------------------------- /part2/phonebook/src/components/NewPersonForm.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | 3 | const NewPersonForm = ({ addNewPerson }) => { 4 | const [newName, setNewName] = useState(""); 5 | const [newNumber, setNewNumber] = useState(""); 6 | 7 | const handleSubmit = (event) => { 8 | event.preventDefault(); 9 | if (newName.trim() === "") return; 10 | addNewPerson({ name: newName, number: newNumber }); 11 | }; 12 | 13 | return ( 14 |
15 |
16 | name:{" "} 17 | setNewName(target.value)} 20 | /> 21 |
22 |
23 | number:{" "} 24 | setNewNumber(target.value)} 27 | /> 28 |
29 |
30 | 31 |
32 |
33 | ); 34 | }; 35 | 36 | export default NewPersonForm; 37 | -------------------------------------------------------------------------------- /part2/phonebook/src/components/Notification.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Notification = ({ message }) => { 4 | if (!message) { 5 | return null; 6 | } 7 | 8 | return
{message.text}
; 9 | }; 10 | 11 | export default Notification; 12 | -------------------------------------------------------------------------------- /part2/phonebook/src/components/Person.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Person = ({ person, handleDelete }) => ( 4 |
5 | {person.name}: {person.number}{" "} 6 | 7 |
8 | ); 9 | 10 | export default Person; 11 | -------------------------------------------------------------------------------- /part2/phonebook/src/components/Persons.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import Person from "./Person"; 4 | 5 | const Persons = ({ persons, handleDelete }) => ( 6 |
7 | {persons.map((person) => ( 8 | 9 | ))} 10 |
11 | ); 12 | 13 | export default Persons; 14 | -------------------------------------------------------------------------------- /part2/phonebook/src/index.css: -------------------------------------------------------------------------------- 1 | h1 { 2 | color: green; 3 | font-style: italic; 4 | } 5 | 6 | .success { 7 | color: green; 8 | background: lightgrey; 9 | font-size: 20px; 10 | border-style: solid; 11 | border-radius: 5px; 12 | padding: 10px; 13 | margin-bottom: 10px; 14 | } 15 | 16 | .error { 17 | color: red; 18 | background: lightgrey; 19 | font-size: 20px; 20 | border-style: solid; 21 | border-radius: 5px; 22 | padding: 10px; 23 | margin-bottom: 10px; 24 | } 25 | 26 | -------------------------------------------------------------------------------- /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 getAll = () => { 6 | const request = axios.get(baseUrl); 7 | return request.then((response) => response.data); 8 | }; 9 | 10 | const create = (personObject) => { 11 | const request = axios.post(baseUrl, personObject); 12 | return request.then((response) => response.data); 13 | }; 14 | 15 | const update = (id, personObject) => { 16 | const request = axios.put(`${baseUrl}/${id}`, personObject); 17 | return request.then((response) => response.data); 18 | }; 19 | 20 | const remove = (id) => { 21 | axios.delete(`${baseUrl}/${id}`); 22 | }; 23 | 24 | export default { getAll, create, update, remove }; 25 | -------------------------------------------------------------------------------- /part3/README.md: -------------------------------------------------------------------------------- 1 | ### Part 3 Programming a server with NodeJS and Express 2 | 3 | ## Solutions to exercises: 4 | 5 | The exercises for this part of the course consist of building a REST backend for the frontend UI built in the last part. 6 | It requires its own repository, therefore it is not included in this main repo and can be found [here](https://github.com/orrsteinberg/fullstackopen-2020-part3) instead. 7 | -------------------------------------------------------------------------------- /part4/.eslintignore: -------------------------------------------------------------------------------- 1 | build 2 | -------------------------------------------------------------------------------- /part4/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | commonjs: true, 4 | es6: true, 5 | node: true, 6 | jest: true, 7 | }, 8 | extends: "eslint:recommended", 9 | globals: { 10 | Atomics: "readonly", 11 | SharedArrayBuffer: "readonly", 12 | }, 13 | parserOptions: { 14 | ecmaVersion: 2018, 15 | }, 16 | rules: { 17 | indent: ["error", 2], 18 | "linebreak-style": ["error", "unix"], 19 | quotes: ["error", "single"], 20 | semi: ["error", "never"], 21 | eqeqeq: "error", 22 | "no-trailing-spaces": "error", 23 | "object-curly-spacing": ["error", "always"], 24 | "arrow-spacing": ["error", { before: true, after: true }], 25 | 'no-console': 0 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /part4/app.js: -------------------------------------------------------------------------------- 1 | const config = require('./utils/config') 2 | const express = require('express') 3 | const cors = require('cors') 4 | require('express-async-errors') 5 | const app = express() 6 | 7 | const blogsRouter = require('./controllers/blogs') 8 | const usersRouter = require('./controllers/users') 9 | const loginRouter = require('./controllers/login') 10 | 11 | const middleware = require('./utils/middleware') 12 | const logger = require('./utils/logger') 13 | const mongoose = require('mongoose') 14 | 15 | // Connect to DB 16 | logger.info('Connecting to', config.MONGODB_URI) 17 | 18 | mongoose.connect(config.MONGODB_URI, 19 | { useNewUrlParser: true, useUnifiedTopology: true, useFindAndModify: false }) 20 | .then(() => logger.info('Connected to MongoDB')) 21 | .catch(error => logger.error('Error connecting to MongoDB', error.message)) 22 | 23 | // Middleware 24 | app.use(cors()) 25 | app.use(express.static('build')) 26 | app.use(express.json()) 27 | 28 | // Custom middleware 29 | app.use(middleware.tokenExtractor) 30 | 31 | // Router 32 | app.use('/api/blogs', blogsRouter) 33 | app.use('/api/users', usersRouter) 34 | app.use('/api/login', loginRouter) 35 | 36 | // Testing enviornment 37 | if (process.env.NODE_ENV === 'test') { 38 | const testingRouter = require('./controllers/testing') 39 | app.use('/api/testing', testingRouter) 40 | } 41 | 42 | // More custom middleware 43 | app.use(middleware.requestLogger) 44 | app.use(middleware.unknownEndpoint) 45 | app.use(middleware.errorHandler) 46 | 47 | module.exports = app 48 | -------------------------------------------------------------------------------- /part4/controllers/login.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken') 2 | const bcrypt = require('bcrypt') 3 | const loginRouter = require('express').Router() 4 | const User = require('../models/user') 5 | 6 | loginRouter.post('/', async (request, response) => { 7 | try { 8 | const { username, password } = request.body 9 | const user = await User.findOne({ username }) 10 | const passwordCorrect = user 11 | ? await bcrypt.compare(password, user.passwordHash) 12 | : false 13 | 14 | if (!user || !passwordCorrect) { 15 | return response.status(401).json({ 16 | error: 'invalid username or password', 17 | }) 18 | } 19 | 20 | const userForToken = { 21 | username: user.username, 22 | id: user._id, 23 | } 24 | 25 | const token = jwt.sign(userForToken, process.env.SECRET) 26 | 27 | response 28 | .status(200) 29 | .send({ token, username: user.username, name: user.name }) 30 | } catch (error) { 31 | response.status(400).end() 32 | } 33 | }) 34 | 35 | module.exports = loginRouter 36 | -------------------------------------------------------------------------------- /part4/controllers/testing.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router() 2 | const Blog = require('../models/blog') 3 | const User = require('../models/user') 4 | 5 | router.post('/reset', async (request, response) => { 6 | await Blog.deleteMany({}) 7 | await User.deleteMany({}) 8 | 9 | response.status(204).end() 10 | }) 11 | 12 | module.exports = router 13 | -------------------------------------------------------------------------------- /part4/controllers/users.js: -------------------------------------------------------------------------------- 1 | const bcrypt = require('bcrypt') 2 | const usersRouter = require('express').Router() 3 | const User = require('../models/user') 4 | 5 | usersRouter.get('/', async (request, response) => { 6 | const users = await User 7 | .find({}) 8 | .populate('blogs', { title: 1, author: 1, url: 1 }) 9 | response.json(users.map(u => u.toJSON())) 10 | }) 11 | 12 | usersRouter.post('/', async (request, response) => { 13 | const body = request.body 14 | 15 | if (!body.username || !body.password) { 16 | return response 17 | .status(400) 18 | .json({ error: 'username and password fields are required' }) 19 | } else if (body.username.length <= 3 || body.password.length <= 3) { 20 | return response 21 | .status(400) 22 | .json({ error: 'username and password have to be at least 3 characters long' }) 23 | } 24 | 25 | const saltRounds = 10 26 | const passwordHash = await bcrypt.hash(body.password, saltRounds) 27 | 28 | const user = new User({ 29 | username: body.username, 30 | name: body.name, 31 | passwordHash: passwordHash 32 | }) 33 | 34 | const savedUser = await user.save() 35 | 36 | response.json(savedUser) 37 | }) 38 | 39 | module.exports = usersRouter 40 | -------------------------------------------------------------------------------- /part4/index.js: -------------------------------------------------------------------------------- 1 | const app = require('./app') 2 | const config = require('./utils/config') 3 | const logger = require('./utils/logger') 4 | 5 | app.listen(config.PORT, () => { 6 | logger.info(`Server running on port ${config.PORT}`) 7 | }) 8 | -------------------------------------------------------------------------------- /part4/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node', 3 | } 4 | -------------------------------------------------------------------------------- /part4/models/blog.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | 3 | // Set up schema 4 | const blogSchema = mongoose.Schema({ 5 | title: { 6 | type: String, 7 | required: true, 8 | minglength: 5 9 | }, 10 | author: { 11 | type: String, 12 | required: true 13 | }, 14 | url: { 15 | type: String, 16 | required: true 17 | }, 18 | likes: { 19 | type: Number, 20 | required: true 21 | }, 22 | user: { 23 | type: mongoose.Schema.Types.ObjectId, 24 | ref: 'User' 25 | } 26 | }) 27 | 28 | blogSchema.set('toJSON', { 29 | transform: (document, returnedObject) => { 30 | returnedObject.id = returnedObject._id.toString() 31 | delete returnedObject._id 32 | delete returnedObject.__v 33 | } 34 | }) 35 | 36 | module.exports = mongoose.model('Blog', blogSchema) 37 | -------------------------------------------------------------------------------- /part4/models/user.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | const uniqueValidator = require('mongoose-unique-validator') 3 | 4 | const userSchema = new mongoose.Schema({ 5 | username: { 6 | type: String, 7 | unique: true 8 | }, 9 | name: String, 10 | passwordHash: String, 11 | blogs: [ 12 | { 13 | type: mongoose.Schema.Types.ObjectId, 14 | ref: 'Blog' 15 | } 16 | ] 17 | }) 18 | 19 | userSchema.plugin(uniqueValidator) 20 | 21 | userSchema.set('toJSON', { 22 | transform: (document, returnedObject) => { 23 | returnedObject.id = returnedObject._id.toString() 24 | delete returnedObject._id 25 | delete returnedObject.__v 26 | // the passwordHash should not be revealed 27 | delete returnedObject.passwordHash 28 | } 29 | }) 30 | 31 | const User = mongoose.model('User', userSchema) 32 | 33 | module.exports = User 34 | -------------------------------------------------------------------------------- /part4/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bloglist-backend", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "cross-env NODE_ENV=production node index.js", 8 | "dev": "cross-env NODE_ENV=development nodemon index.js", 9 | "test": "cross-env NODE_ENV=test jest --verbose --runInBand", 10 | "lint": "eslint .", 11 | "lint:fix": "eslint . --fix", 12 | "start:test": "cross-env NODE_ENV=test node index.js" 13 | }, 14 | "author": "", 15 | "license": "ISC", 16 | "dependencies": { 17 | "bcrypt": "^4.0.1", 18 | "cors": "^2.8.5", 19 | "dotenv": "^8.2.0", 20 | "express": "^4.17.1", 21 | "express-async-errors": "^3.1.1", 22 | "jsonwebtoken": "^8.5.1", 23 | "mongoose": "^5.9.7", 24 | "mongoose-unique-validator": "^2.0.3" 25 | }, 26 | "devDependencies": { 27 | "cross-env": "^7.0.2", 28 | "eslint": "^6.8.0", 29 | "jest": "^25.2.7", 30 | "nodemon": "^2.0.2", 31 | "supertest": "^4.0.2" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /part4/utils/config.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | 3 | let PORT = process.env.PORT 4 | let MONGODB_URI = process.env.MONGODB_URI 5 | 6 | if (process.env.NODE_ENV === 'test') { 7 | MONGODB_URI = process.env.TEST_MONGODB_URI 8 | } 9 | 10 | module.exports = { PORT, MONGODB_URI } 11 | -------------------------------------------------------------------------------- /part4/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, error 13 | } 14 | -------------------------------------------------------------------------------- /part4/utils/middleware.js: -------------------------------------------------------------------------------- 1 | // Custom middleware 2 | require('dotenv').config() 3 | const jwt = require('jsonwebtoken') 4 | const logger = require('./logger') 5 | 6 | const requestLogger = (request, response, next) => { 7 | logger.info('Method:', request.method) 8 | logger.info('Path: ', request.path) 9 | logger.info('Body: ', request.body) 10 | logger.info('---') 11 | next() 12 | } 13 | 14 | const unknownEndpoint = (request, response) => { 15 | response.status(404).send({ error: 'unknown endpoint' }) 16 | } 17 | 18 | const errorHandler = (error, request, response, next) => { 19 | logger.error(error.message) 20 | 21 | if (error.name === 'CastError' && error.kind === 'ObjectId') { 22 | return response.status(400).send({ error: 'malformatted id' }) 23 | } else if (error.name === 'ValidationError') { 24 | return response.status(400).json({ error: error.message }) 25 | } else if (error.name === 'JsonWebTokenError') { 26 | return response.status(400).json({ error: 'invalid token' }) 27 | } 28 | 29 | next(error) 30 | } 31 | 32 | // Get user token from request 33 | const tokenExtractor = (request, response, next) => { 34 | const authorization = request.get('authorization') 35 | 36 | // Check if there is a token and extract it 37 | if (authorization && authorization.toLowerCase().startsWith('bearer')) { 38 | request.token = authorization.substring(7) 39 | } else { 40 | request.token = null 41 | } 42 | 43 | // Check if the token is valid 44 | try { 45 | const decodedToken = jwt.verify(request.token, process.env.SECRET) 46 | request.decodedToken = decodedToken 47 | } catch (error) { 48 | request.decodedToken = null 49 | } 50 | 51 | next() 52 | } 53 | 54 | module.exports = { 55 | requestLogger, 56 | unknownEndpoint, 57 | errorHandler, 58 | tokenExtractor, 59 | } 60 | -------------------------------------------------------------------------------- /part5/.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | -------------------------------------------------------------------------------- /part5/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es6: true, 5 | 'jest/globals': true, 6 | 'cypress/globals': true 7 | }, 8 | extends: ['eslint:recommended', 'plugin:react/recommended'], 9 | parserOptions: { 10 | ecmaFeatures: { 11 | jsx: true, 12 | }, 13 | ecmaVersion: 2018, 14 | sourceType: 'module', 15 | }, 16 | plugins: ['react', 'jest', 'cypress'], 17 | rules: { 18 | indent: ['error', 2], 19 | 'linebreak-style': ['error', 'unix'], 20 | quotes: ['error', 'single'], 21 | semi: ['error', 'never'], 22 | eqeqeq: 'error', 23 | 'no-trailing-spaces': 'error', 24 | 'object-curly-spacing': ['error', 'always'], 25 | 'arrow-spacing': ['error', { before: true, after: true }], 26 | 'no-console': 0, 27 | 'react/prop-types': 0, 28 | }, 29 | settings: { 30 | react: { 31 | version: 'detect', 32 | }, 33 | }, 34 | }; 35 | -------------------------------------------------------------------------------- /part5/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "singleQuote": true, 4 | "semi": false 5 | } 6 | -------------------------------------------------------------------------------- /part5/cypress.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /part5/cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } -------------------------------------------------------------------------------- /part5/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/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 | 27 | Cypress.Commands.add('login', ({ username, password }) => { 28 | cy.request('POST', 'http://localhost:3001/api/login', { 29 | username, 30 | password, 31 | }).then((response) => { 32 | localStorage.setItem('loggedInUser', JSON.stringify(response.body)) 33 | cy.visit('http://localhost:3000') 34 | }) 35 | }) 36 | 37 | Cypress.Commands.add('createBlog', ({ title, author, url, likes }) => { 38 | const content = likes ? { title, author, url, likes } : { title, author, url } 39 | cy.request({ 40 | url: 'http://localhost:3001/api/blogs', 41 | method: 'POST', 42 | body: content, 43 | headers: { 44 | Authorization: `bearer ${JSON.parse(localStorage.getItem('loggedInUser')).token}`, 45 | }, 46 | }) 47 | 48 | cy.visit('http://localhost:3000') 49 | }) 50 | 51 | Cypress.Commands.add('addLike', (blogTitle) => { 52 | cy.get('.blog').contains(blogTitle).parent().parent().contains('Add').click() 53 | }) 54 | -------------------------------------------------------------------------------- /part5/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/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bloglist-frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/user-event": "^7.2.1", 7 | "axios": "^0.19.2", 8 | "prop-types": "^15.7.2", 9 | "react": "^16.12.0", 10 | "react-dom": "^16.12.0", 11 | "react-scripts": "^3.4.3" 12 | }, 13 | "scripts": { 14 | "start": "react-scripts start", 15 | "build": "react-scripts build", 16 | "test": "react-scripts test", 17 | "eject": "react-scripts eject", 18 | "lint": "eslint .", 19 | "lint:fix": "eslint . --fix", 20 | "cypress:open": "cypress open", 21 | "test:e2e": "cypress run" 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 | "proxy": "http://localhost:3001", 39 | "devDependencies": { 40 | "@testing-library/jest-dom": "^4.2.4", 41 | "@testing-library/react": "^9.5.0", 42 | "cypress": "^4.4.1", 43 | "eslint-plugin-cypress": "^2.11.2", 44 | "eslint-plugin-jest": "^23.8.2", 45 | "eslint-plugin-react": "^7.21.4" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /part5/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orrsteinberg/fullstackopen-2020/2565b48898ac7249e645ba3ba96fdef4780b782a/part5/public/favicon.ico -------------------------------------------------------------------------------- /part5/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /part5/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orrsteinberg/fullstackopen-2020/2565b48898ac7249e645ba3ba96fdef4780b782a/part5/public/logo192.png -------------------------------------------------------------------------------- /part5/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orrsteinberg/fullstackopen-2020/2565b48898ac7249e645ba3ba96fdef4780b782a/part5/public/logo512.png -------------------------------------------------------------------------------- /part5/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 | -------------------------------------------------------------------------------- /part5/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /part5/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react' 2 | import Header from './components/Header' 3 | import Bloglist from './components/Bloglist' 4 | import LoginForm from './components/LoginForm' 5 | import blogService from './services/blogs' 6 | 7 | const App = () => { 8 | const [user, setUser] = useState(null) 9 | const [notification, setNotification] = useState({ 10 | message: null, 11 | type: null, 12 | }) 13 | 14 | useEffect(() => { 15 | const loggedInJSON = window.localStorage.getItem('loggedInUser') 16 | if (loggedInJSON) { 17 | const user = JSON.parse(loggedInJSON) 18 | setUser(user) 19 | blogService.setToken(user.token) 20 | } 21 | }, []) 22 | 23 | const notify = (notification) => { 24 | setNotification(notification) 25 | setTimeout(() => { 26 | setNotification({ message: null, type: null }) 27 | }, 5000) 28 | } 29 | 30 | const login = (user) => { 31 | window.localStorage.setItem('loggedInUser', JSON.stringify(user)) 32 | blogService.setToken(user.token) 33 | setUser(user) 34 | } 35 | 36 | const handleLogOut = () => { 37 | window.localStorage.clear() 38 | setUser(null) 39 | blogService.clearToken() 40 | } 41 | 42 | if (!user) { 43 | return ( 44 |
45 |
46 | 47 |
48 | ) 49 | } 50 | 51 | return ( 52 |
53 |
54 |

{user.name} logged in

55 | 56 | 57 |
58 | ) 59 | } 60 | 61 | export default App 62 | -------------------------------------------------------------------------------- /part5/src/components/CreateBlogForm.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | const CreateBlogForm = ({ createNewBlog }) => { 5 | const [title, setTitle] = useState('') 6 | const [author, setAuthor] = useState('') 7 | const [url, setUrl] = useState('') 8 | 9 | const addNewBlog = (event) => { 10 | event.preventDefault() 11 | createNewBlog({ 12 | title, 13 | author, 14 | url, 15 | }) 16 | 17 | setTitle('') 18 | setAuthor('') 19 | setUrl('') 20 | } 21 | 22 | return ( 23 |
24 |

Create new

25 |
26 |
27 | Title:{' '} 28 | setTitle(target.value)} 34 | /> 35 |
36 |
37 | Author:{' '} 38 | setAuthor(target.value)} 44 | /> 45 |
46 |
47 | URL:{' '} 48 | setUrl(target.value)} 54 | /> 55 |
56 | 57 |
58 |
59 | ) 60 | } 61 | 62 | CreateBlogForm.propTypes = { 63 | createNewBlog: PropTypes.func.isRequired, 64 | } 65 | 66 | export default CreateBlogForm 67 | -------------------------------------------------------------------------------- /part5/src/components/CreateBlogForm.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render, fireEvent } from '@testing-library/react' 3 | import '@testing-library/jest-dom/extend-expect' 4 | import CreateBlogForm from './CreateBlogForm' 5 | 6 | describe(' component', () => { 7 | test('updates parent state and calls handler onSubmit', () => { 8 | const createBlog = jest.fn() 9 | 10 | const component = render() 11 | 12 | const title = component.container.querySelector('#title') 13 | const author = component.container.querySelector('#author') 14 | const url = component.container.querySelector('#url') 15 | const form = component.container.querySelector('#create-blog-form') 16 | 17 | fireEvent.change(title, { 18 | target: { value: 'New Blog Title' }, 19 | }) 20 | fireEvent.change(author, { 21 | target: { value: 'New Blog Author' }, 22 | }) 23 | fireEvent.change(url, { 24 | target: { value: 'New Blog Url' }, 25 | }) 26 | fireEvent.submit(form) 27 | 28 | // createBlog handler function gets called onSubmit 29 | expect(createBlog.mock.calls).toHaveLength(1) 30 | 31 | // with the right data 32 | expect(createBlog.mock.calls[0][0].title).toBe('New Blog Title') 33 | expect(createBlog.mock.calls[0][0].author).toBe('New Blog Author') 34 | expect(createBlog.mock.calls[0][0].url).toBe('New Blog Url') 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /part5/src/components/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import Notification from '../components/Notification' 4 | 5 | const Header = ({ title, notification }) => ( 6 |
7 |

{title}

8 | 9 |
10 | ) 11 | 12 | Header.propTypes = { 13 | title: PropTypes.string.isRequired, 14 | notification: PropTypes.object.isRequired, 15 | } 16 | export default Header 17 | -------------------------------------------------------------------------------- /part5/src/components/LoginForm.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import PropTypes from 'prop-types' 3 | import loginService from '../services/login' 4 | 5 | const LoginForm = ({ login, notify }) => { 6 | const [username, setUsername] = useState('') 7 | const [password, setPassword] = useState('') 8 | 9 | const handleLogin = async (event) => { 10 | event.preventDefault() 11 | 12 | try { 13 | const user = await loginService.login({ username, password }) 14 | login(user) 15 | 16 | notify({ 17 | message: `${user.name} logged in`, 18 | type: 'success', 19 | }) 20 | } catch (error) { 21 | notify({ 22 | message: 'Wrong username or password', 23 | type: 'error', 24 | }) 25 | } 26 | } 27 | 28 | return ( 29 |
30 |
31 |
32 | Username:{' '} 33 | setUsername(target.value)} 39 | /> 40 |
41 |
42 | Password:{' '} 43 | setPassword(target.value)} 49 | /> 50 |
51 | 52 |
53 |
54 | ) 55 | } 56 | 57 | LoginForm.propTypes = { 58 | login: PropTypes.func.isRequired, 59 | notify: PropTypes.func.isRequired, 60 | } 61 | export default LoginForm 62 | -------------------------------------------------------------------------------- /part5/src/components/Notification.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | const Notification = ({ notification }) => { 5 | const { message, type } = notification 6 | 7 | if (!message) { 8 | return null 9 | } 10 | 11 | return
{message}
12 | } 13 | 14 | Notification.propTypes = { 15 | notification: PropTypes.object.isRequired, 16 | } 17 | 18 | export default Notification 19 | -------------------------------------------------------------------------------- /part5/src/components/Togglable.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useImperativeHandle } from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | const Togglable = React.forwardRef((props, ref) => { 5 | const [visible, setVisible] = useState(false) 6 | 7 | const hideWhenVisible = { display: visible ? 'none' : '' } 8 | const ShowWhenVisible = { display: visible ? '' : 'none' } 9 | 10 | const toggleVisibility = () => setVisible(!visible) 11 | 12 | useImperativeHandle(ref, () => { 13 | return { 14 | toggleVisibility 15 | } 16 | }) 17 | 18 | return ( 19 |
20 |
21 | 22 |
23 |
24 | {props.children} 25 | 26 |
27 |
28 | ) 29 | }) 30 | 31 | Togglable.propTypes = { 32 | buttonLabel: PropTypes.string.isRequired 33 | } 34 | 35 | Togglable.displayName = 'Toggleable' 36 | 37 | export default Togglable 38 | -------------------------------------------------------------------------------- /part5/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 | .error { 11 | color: red; 12 | background: lightgrey; 13 | font-size: 20px; 14 | border-style: solid; 15 | border-radius: 5px; 16 | padding: 10px; 17 | margin-bottom: 10px; 18 | } 19 | 20 | .success { 21 | color: green; 22 | background: lightgrey; 23 | font-size: 20px; 24 | border-style: solid; 25 | border-radius: 5px; 26 | padding: 10px; 27 | margin-bottom: 10px; 28 | } 29 | -------------------------------------------------------------------------------- /part5/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 | -------------------------------------------------------------------------------- /part5/src/services/blogs.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | const baseUrl = '/api/blogs' 3 | 4 | let token = null 5 | 6 | const setToken = (newToken) => { 7 | token = `bearer ${newToken}` 8 | } 9 | 10 | const clearToken = () => (token = null) 11 | 12 | const getAll = async () => { 13 | const response = await axios.get(baseUrl) 14 | return response.data 15 | } 16 | 17 | const create = async (newObject) => { 18 | const config = { 19 | headers: { Authorization: token }, 20 | } 21 | 22 | const response = await axios.post(baseUrl, newObject, config) 23 | 24 | return response.data 25 | } 26 | 27 | const update = async (blogId, newObject) => { 28 | const config = { 29 | headers: { Authorization: token }, 30 | } 31 | 32 | const response = await axios.put(`${baseUrl}/${blogId}`, newObject, config) 33 | 34 | return response.data 35 | } 36 | 37 | const remove = async (blogId) => { 38 | const config = { 39 | headers: { Authorization: token }, 40 | } 41 | 42 | const response = await axios.delete(`${baseUrl}/${blogId}`, config) 43 | return response 44 | } 45 | 46 | export default { getAll, create, update, remove, setToken, clearToken } 47 | -------------------------------------------------------------------------------- /part5/src/services/login.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | const baseUrl = '/api/login' 3 | 4 | const login = async credentials => { 5 | const response = await axios.post(baseUrl, credentials) 6 | return response.data 7 | } 8 | 9 | export default { login } 10 | -------------------------------------------------------------------------------- /part5/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 | -------------------------------------------------------------------------------- /part6/redux-anecdotes/db.json: -------------------------------------------------------------------------------- 1 | { 2 | "anecdotes": [ 3 | { 4 | "content": "If it hurts, do it more often", 5 | "id": "47145", 6 | "votes": 0 7 | }, 8 | { 9 | "content": "Adding manpower to a late software project makes it later!", 10 | "id": "21149", 11 | "votes": 0 12 | }, 13 | { 14 | "content": "The first 90 percent of the code accounts for the first 90 percent of the development time...The remaining 10 percent of the code accounts for the other 90 percent of the development time.", 15 | "id": "69581", 16 | "votes": 0 17 | }, 18 | { 19 | "content": "Any fool can write code that a computer can understand. Good programmers write code that humans can understand.", 20 | "id": "36975", 21 | "votes": 0 22 | }, 23 | { 24 | "content": "Premature optimization is the root of all evil.", 25 | "id": "25170", 26 | "votes": 0 27 | }, 28 | { 29 | "content": "Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it.", 30 | "id": "98312", 31 | "votes": 0 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /part6/redux-anecdotes/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-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 | "axios": "^0.20.0", 10 | "react": "^16.12.0", 11 | "react-dom": "^16.12.0", 12 | "react-redux": "^7.1.3", 13 | "react-scripts": "3.3.1", 14 | "redux": "^4.0.5", 15 | "redux-thunk": "^2.3.0" 16 | }, 17 | "scripts": { 18 | "start": "react-scripts start", 19 | "build": "react-scripts build", 20 | "test": "react-scripts test", 21 | "eject": "react-scripts eject", 22 | "server": "json-server -p3001 --watch db.json" 23 | }, 24 | "eslintConfig": { 25 | "extends": "react-app" 26 | }, 27 | "browserslist": { 28 | "production": [ 29 | ">0.2%", 30 | "not dead", 31 | "not op_mini all" 32 | ], 33 | "development": [ 34 | "last 1 chrome version", 35 | "last 1 firefox version", 36 | "last 1 safari version" 37 | ] 38 | }, 39 | "devDependencies": { 40 | "json-server": "^0.16.2" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /part6/redux-anecdotes/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orrsteinberg/fullstackopen-2020/2565b48898ac7249e645ba3ba96fdef4780b782a/part6/redux-anecdotes/public/favicon.ico -------------------------------------------------------------------------------- /part6/redux-anecdotes/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /part6/redux-anecdotes/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orrsteinberg/fullstackopen-2020/2565b48898ac7249e645ba3ba96fdef4780b782a/part6/redux-anecdotes/public/logo192.png -------------------------------------------------------------------------------- /part6/redux-anecdotes/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orrsteinberg/fullstackopen-2020/2565b48898ac7249e645ba3ba96fdef4780b782a/part6/redux-anecdotes/public/logo512.png -------------------------------------------------------------------------------- /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 | "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 | -------------------------------------------------------------------------------- /part6/redux-anecdotes/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /part6/redux-anecdotes/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { useDispatch } from "react-redux"; 3 | import { initializeAnecdotes } from "./reducers/anecdoteReducer"; 4 | import Notification from "./components/Notification"; 5 | import AnecdoteList from "./components/AnecdoteList"; 6 | import AnecdoteForm from "./components/AnecdoteForm"; 7 | 8 | const App = () => { 9 | const dispatch = useDispatch(); 10 | 11 | useEffect(() => { 12 | dispatch(initializeAnecdotes()); 13 | }, [dispatch]); 14 | 15 | return ( 16 |
17 |

Anecdotes

18 | 19 | 20 |

create new

21 | 22 |
23 | ); 24 | }; 25 | 26 | export default App; 27 | -------------------------------------------------------------------------------- /part6/redux-anecdotes/src/components/AnecdoteForm.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { connect } from "react-redux"; 3 | import { createAnecdote } from "../reducers/anecdoteReducer"; 4 | import { setNotification } from "../reducers/notificationReducer"; 5 | 6 | const AnecdoteForm = (props) => { 7 | const addNew = (event) => { 8 | event.preventDefault(); 9 | 10 | const content = event.target.anecdote.value; 11 | props.createAnecdote(content); 12 | // Display message 13 | props.setNotification(`Created new anecdote: ${content}`, 5); 14 | }; 15 | 16 | return ( 17 |
18 |
19 | 20 |
21 | 22 |
23 | ); 24 | }; 25 | 26 | // Connecting component to redux store using the connect() function 27 | 28 | const mapDispatchToProps = { 29 | createAnecdote, 30 | setNotification, 31 | }; 32 | 33 | const connectedAnecdoteForm = connect(null, mapDispatchToProps)(AnecdoteForm); 34 | export default connectedAnecdoteForm; 35 | -------------------------------------------------------------------------------- /part6/redux-anecdotes/src/components/AnecdoteList.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { connect } from "react-redux"; 3 | import { addVote } from "../reducers/anecdoteReducer"; 4 | import { setNotification } from "../reducers/notificationReducer"; 5 | import Filter from "./Filter"; 6 | 7 | const AnecdoteList = ({ anecdotes, addVote, setNotification }) => { 8 | const vote = (id) => { 9 | addVote(id); 10 | const content = anecdotes.find((a) => a.id === id).content; 11 | // Display message 12 | setNotification(`You voted for: ${content}`, 5); 13 | }; 14 | 15 | return ( 16 |
17 | 18 | {anecdotes.map(({ id, content, votes }) => ( 19 |
20 |
{content}
21 |
22 | has {votes} 23 | 24 |
25 |
26 | ))} 27 |
28 | ); 29 | }; 30 | 31 | // Connecting component to redux store using the connect() function 32 | 33 | const mapStateToProps = (state) => { 34 | // Map anecdotes in state to a filtered and sorted anecdotes prop 35 | if (state.filter === "") { 36 | return { 37 | anecdotes: state.anecdotes.sort((a, b) => b.votes - a.votes), 38 | }; 39 | } 40 | 41 | const filterAnecdotes = (anecdote) => 42 | anecdote.content.toLowerCase().includes(state.filter.toLowerCase()); 43 | 44 | return { 45 | anecdotes: state.anecdotes 46 | .filter(filterAnecdotes) 47 | .sort((a, b) => b.votes - a.votes), 48 | }; 49 | }; 50 | 51 | const mapDispatchToProps = { 52 | setNotification, 53 | addVote, 54 | }; 55 | 56 | const connectedAnecdoteList = connect( 57 | mapStateToProps, 58 | mapDispatchToProps 59 | )(AnecdoteList); 60 | 61 | export default connectedAnecdoteList; 62 | -------------------------------------------------------------------------------- /part6/redux-anecdotes/src/components/Filter.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { connect } from "react-redux"; 3 | import { updateFilter } from "../reducers/filterReducer"; 4 | 5 | const style = { 6 | marginBottom: 10, 7 | }; 8 | 9 | const Filter = (props) => { 10 | const handleChange = (event) => { 11 | const filterInput = event.target.value; 12 | props.updateFilter(filterInput); 13 | }; 14 | 15 | return ( 16 |
17 | filter 18 |
19 | ); 20 | }; 21 | 22 | // Connecting component to redux store using the connect() function 23 | 24 | const connectedFilter = connect(null, { updateFilter })(Filter); 25 | 26 | export default connectedFilter; 27 | -------------------------------------------------------------------------------- /part6/redux-anecdotes/src/components/Notification.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { connect } from "react-redux"; 3 | 4 | const style = { 5 | border: "solid", 6 | padding: 10, 7 | borderWidth: 1, 8 | marginBottom: 10, 9 | }; 10 | 11 | const Notification = ({ notification }) => { 12 | return notification ?
{notification}
: null; 13 | }; 14 | 15 | // Connecting component to redux store using the connect() function 16 | 17 | const mapStateToProps = (state) => ({ notification: state.notification }); 18 | 19 | const connectedNotification = connect(mapStateToProps)(Notification); 20 | 21 | export default connectedNotification; 22 | -------------------------------------------------------------------------------- /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 store from "./store"; 5 | import App from "./App"; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById("root") 12 | ); 13 | -------------------------------------------------------------------------------- /part6/redux-anecdotes/src/reducers/anecdoteReducer.js: -------------------------------------------------------------------------------- 1 | import anecdoteService from "../services/anecdotes"; 2 | 3 | // Actions 4 | export const initializeAnecdotes = () => { 5 | return async (dispatch) => { 6 | const anecdotes = await anecdoteService.getAll(); 7 | dispatch({ 8 | type: "INIT_ANECDOTES", 9 | data: anecdotes, 10 | }); 11 | }; 12 | }; 13 | 14 | export const addVote = (id) => { 15 | return async (dispatch) => { 16 | const updatedAnecdote = await anecdoteService.addVote(id); 17 | dispatch({ 18 | type: "ADD_VOTE", 19 | data: updatedAnecdote, 20 | }); 21 | }; 22 | }; 23 | 24 | export const createAnecdote = (content) => { 25 | return async (dispatch) => { 26 | const anecdote = await anecdoteService.createNew(content); 27 | dispatch({ 28 | type: "NEW_ANECDOTE", 29 | data: anecdote, 30 | }); 31 | }; 32 | }; 33 | 34 | // Reducer 35 | const reducer = (state = [], action) => { 36 | const { type, data } = action; 37 | 38 | switch (type) { 39 | case "INIT_ANECDOTES": 40 | return data; 41 | 42 | case "NEW_ANECDOTE": 43 | return [...state, data]; 44 | 45 | case "ADD_VOTE": 46 | return state.map((anecdote) => 47 | anecdote.id === data.id 48 | ? { ...anecdote, votes: anecdote.votes + 1 } 49 | : anecdote 50 | ); 51 | 52 | default: 53 | return state; 54 | } 55 | }; 56 | 57 | export default reducer; 58 | -------------------------------------------------------------------------------- /part6/redux-anecdotes/src/reducers/filterReducer.js: -------------------------------------------------------------------------------- 1 | export const updateFilter = (filterInput) => ({ 2 | type: "UPDATE_FILTER", 3 | data: filterInput, 4 | }); 5 | 6 | const filterReducer = (state = "", action) => { 7 | const { type, data } = action; 8 | 9 | switch (type) { 10 | case "UPDATE_FILTER": 11 | return data; 12 | default: 13 | return state; 14 | } 15 | }; 16 | 17 | export default filterReducer; 18 | -------------------------------------------------------------------------------- /part6/redux-anecdotes/src/reducers/notificationReducer.js: -------------------------------------------------------------------------------- 1 | export const setNotification = (message, duration) => { 2 | if (window._anecdotesNotificationTimeout) { 3 | window.clearTimeout(window._anecdotesNotificationTimeout); 4 | } 5 | 6 | return async (dispatch) => { 7 | dispatch({ 8 | type: "DISPLAY_NOTIFICATION", 9 | data: message, 10 | }); 11 | 12 | window._anecdotesNotificationTimeout = setTimeout( 13 | () => 14 | dispatch({ 15 | type: "CLEAR_NOTIFICATION", 16 | }), 17 | duration * 1000 18 | ); 19 | }; 20 | }; 21 | 22 | const notificationReducer = (state = null, action) => { 23 | const { type, data } = action; 24 | 25 | switch (type) { 26 | case "DISPLAY_NOTIFICATION": 27 | return data; 28 | case "CLEAR_NOTIFICATION": 29 | return null; 30 | default: 31 | return state; 32 | } 33 | }; 34 | 35 | export default notificationReducer; 36 | -------------------------------------------------------------------------------- /part6/redux-anecdotes/src/services/anecdotes.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | const baseUrl = "http://localhost:3001/anecdotes"; 4 | 5 | const getAll = async () => { 6 | const response = await axios.get(baseUrl); 7 | return response.data; 8 | }; 9 | 10 | const createNew = async (content) => { 11 | const anecdoteObject = { content, votes: 0 }; 12 | const response = await axios.post(baseUrl, anecdoteObject); 13 | return response.data; 14 | }; 15 | 16 | const addVote = async (id) => { 17 | const { data: anecdoteToUpdate } = await axios.get(`${baseUrl}/${id}`); 18 | const anecdoteObject = { ...anecdoteToUpdate, votes: anecdoteToUpdate.votes + 1 } 19 | const response = await axios.put(`${baseUrl}/${id}`, anecdoteObject); 20 | return response.data; 21 | } 22 | 23 | export default { 24 | getAll, 25 | createNew, 26 | addVote 27 | }; 28 | -------------------------------------------------------------------------------- /part6/redux-anecdotes/src/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, combineReducers, applyMiddleware } from "redux"; 2 | import thunk from "redux-thunk"; 3 | import anecdoteReducer from "./reducers/anecdoteReducer"; 4 | import notificationReducer from "./reducers/notificationReducer"; 5 | import filterReducer from "./reducers/filterReducer"; 6 | 7 | const reducer = combineReducers({ 8 | anecdotes: anecdoteReducer, 9 | notification: notificationReducer, 10 | filter: filterReducer, 11 | }); 12 | 13 | const store = createStore(reducer, applyMiddleware(thunk)); 14 | 15 | export default store; 16 | -------------------------------------------------------------------------------- /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.3.1", 12 | "redux": "^4.0.5" 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 | "devDependencies": { 36 | "deep-freeze": "0.0.1" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /part6/unicafe-redux/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orrsteinberg/fullstackopen-2020/2565b48898ac7249e645ba3ba96fdef4780b782a/part6/unicafe-redux/public/favicon.ico -------------------------------------------------------------------------------- /part6/unicafe-redux/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /part6/unicafe-redux/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orrsteinberg/fullstackopen-2020/2565b48898ac7249e645ba3ba96fdef4780b782a/part6/unicafe-redux/public/logo192.png -------------------------------------------------------------------------------- /part6/unicafe-redux/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orrsteinberg/fullstackopen-2020/2565b48898ac7249e645ba3ba96fdef4780b782a/part6/unicafe-redux/public/logo512.png -------------------------------------------------------------------------------- /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 | "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 | -------------------------------------------------------------------------------- /part6/unicafe-redux/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /part6/unicafe-redux/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import { createStore } from "redux"; 4 | import reducer from "./reducer"; 5 | 6 | const store = createStore(reducer); 7 | 8 | const App = () => { 9 | const state = store.getState(); 10 | 11 | const good = () => { 12 | store.dispatch({ 13 | type: "GOOD", 14 | }); 15 | }; 16 | 17 | const ok = () => { 18 | store.dispatch({ 19 | type: "OK", 20 | }); 21 | }; 22 | 23 | const bad = () => { 24 | store.dispatch({ 25 | type: "BAD", 26 | }); 27 | }; 28 | 29 | const zero = () => { 30 | store.dispatch({ 31 | type: "ZERO", 32 | }); 33 | }; 34 | 35 | return ( 36 |
37 | 38 | 39 | 40 | 41 |
good {state.good}
42 |
neutral {state.ok}
43 |
bad {state.bad}
44 |
45 | ); 46 | }; 47 | 48 | const renderApp = () => { 49 | ReactDOM.render(, document.getElementById("root")); 50 | }; 51 | 52 | renderApp(); 53 | store.subscribe(renderApp); 54 | -------------------------------------------------------------------------------- /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 | console.log(action); 9 | switch (action.type) { 10 | case "GOOD": 11 | return { 12 | ...state, 13 | good: state.good + 1, 14 | }; 15 | case "OK": 16 | return { 17 | ...state, 18 | ok: state.ok + 1, 19 | }; 20 | case "BAD": 21 | return { 22 | ...state, 23 | bad: state.bad + 1, 24 | }; 25 | case "ZERO": 26 | return { 27 | good: 0, 28 | ok: 0, 29 | bad: 0, 30 | }; 31 | default: 32 | return state; 33 | } 34 | }; 35 | 36 | export default counterReducer; 37 | -------------------------------------------------------------------------------- /part6/unicafe-redux/src/reducer.test.js: -------------------------------------------------------------------------------- 1 | import deepFreeze from "deep-freeze"; 2 | import counterReducer from "./reducer"; 3 | 4 | describe("unicafe reducer", () => { 5 | const initialState = { 6 | good: 0, 7 | ok: 0, 8 | bad: 0, 9 | }; 10 | 11 | test("should return a proper initial state when called with undefined state", () => { 12 | const state = {}; 13 | const action = { 14 | type: "DO_NOTHING", 15 | }; 16 | 17 | const newState = counterReducer(undefined, action); 18 | expect(newState).toEqual(initialState); 19 | }); 20 | 21 | test("good is incremented", () => { 22 | const action = { 23 | type: "GOOD", 24 | }; 25 | const state = initialState; 26 | 27 | deepFreeze(state); 28 | const newState = counterReducer(state, action); 29 | expect(newState).toEqual({ 30 | good: 1, 31 | ok: 0, 32 | bad: 0, 33 | }); 34 | }); 35 | 36 | test("ok is incremented", () => { 37 | const action = { 38 | type: "OK", 39 | }; 40 | const state = initialState; 41 | 42 | deepFreeze(state); 43 | const newState = counterReducer(state, action); 44 | expect(newState).toEqual({ 45 | good: 0, 46 | ok: 1, 47 | bad: 0, 48 | }); 49 | }); 50 | 51 | test("bad is incremented", () => { 52 | const action = { 53 | type: "BAD", 54 | }; 55 | const state = initialState; 56 | 57 | deepFreeze(state); 58 | const newState = counterReducer(state, action); 59 | expect(newState).toEqual({ 60 | good: 0, 61 | ok: 0, 62 | bad: 1, 63 | }); 64 | }); 65 | 66 | test("zero resets the state back to initial state", () => { 67 | const action = { 68 | type: "ZERO", 69 | }; 70 | const state = { 71 | good: 1, 72 | ok: 1, 73 | bad: 1, 74 | }; 75 | 76 | deepFreeze(state); 77 | const newState = counterReducer(state, action); 78 | expect(newState).toEqual(initialState); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /part7/bloglist-backend/.eslintignore: -------------------------------------------------------------------------------- 1 | build 2 | -------------------------------------------------------------------------------- /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: "eslint:recommended", 9 | globals: { 10 | Atomics: "readonly", 11 | SharedArrayBuffer: "readonly", 12 | }, 13 | parserOptions: { 14 | ecmaVersion: 2018, 15 | }, 16 | rules: { 17 | indent: ["error", 2], 18 | "linebreak-style": ["error", "unix"], 19 | quotes: ["error", "single"], 20 | semi: ["error", "never"], 21 | eqeqeq: "error", 22 | "no-trailing-spaces": "error", 23 | "object-curly-spacing": ["error", "always"], 24 | "arrow-spacing": ["error", { before: true, after: true }], 25 | 'no-console': 0 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /part7/bloglist-backend/app.js: -------------------------------------------------------------------------------- 1 | const config = require('./utils/config') 2 | const express = require('express') 3 | const cors = require('cors') 4 | require('express-async-errors') 5 | const app = express() 6 | 7 | const blogsRouter = require('./controllers/blogs') 8 | const usersRouter = require('./controllers/users') 9 | const loginRouter = require('./controllers/login') 10 | 11 | const middleware = require('./utils/middleware') 12 | const logger = require('./utils/logger') 13 | const mongoose = require('mongoose') 14 | 15 | // Connect to DB 16 | logger.info('Connecting to', config.MONGODB_URI) 17 | 18 | mongoose 19 | .connect(config.MONGODB_URI, { 20 | useNewUrlParser: true, 21 | useUnifiedTopology: true, 22 | useFindAndModify: false, 23 | }) 24 | .then(() => logger.info('Connected to MongoDB')) 25 | .catch((error) => logger.error('Error connecting to MongoDB', error.message)) 26 | 27 | // Middleware 28 | app.use(cors()) 29 | app.use(express.static('build')) 30 | app.use(express.json()) 31 | 32 | // Custom middleware 33 | app.use(middleware.tokenExtractor) 34 | 35 | // Router 36 | app.use('/api/blogs', blogsRouter) 37 | app.use('/api/users', usersRouter) 38 | app.use('/api/login', loginRouter) 39 | 40 | // Testing enviornment 41 | if (process.env.NODE_ENV === 'test') { 42 | const testingRouter = require('./controllers/testing') 43 | app.use('/api/testing', testingRouter) 44 | } 45 | 46 | // More custom middleware 47 | app.use(middleware.requestLogger) 48 | app.use(middleware.unknownEndpoint) 49 | app.use(middleware.errorHandler) 50 | 51 | module.exports = app 52 | -------------------------------------------------------------------------------- /part7/bloglist-backend/controllers/login.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken') 2 | const bcrypt = require('bcrypt') 3 | const loginRouter = require('express').Router() 4 | const User = require('../models/user') 5 | 6 | loginRouter.post('/', async (request, response) => { 7 | try { 8 | const { username, password } = request.body 9 | const user = await User.findOne({ username }) 10 | const passwordCorrect = user 11 | ? await bcrypt.compare(password, user.passwordHash) 12 | : false 13 | 14 | if (!user || !passwordCorrect) { 15 | return response.status(401).json({ 16 | error: 'invalid username or password', 17 | }) 18 | } 19 | 20 | const userForToken = { 21 | username: user.username, 22 | id: user._id, 23 | } 24 | 25 | const token = jwt.sign(userForToken, process.env.SECRET) 26 | 27 | response 28 | .status(200) 29 | .send({ token, username: user.username, name: user.name }) 30 | } catch (error) { 31 | response.status(400).end() 32 | } 33 | }) 34 | 35 | module.exports = loginRouter 36 | -------------------------------------------------------------------------------- /part7/bloglist-backend/controllers/testing.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router() 2 | const Blog = require('../models/blog') 3 | const User = require('../models/user') 4 | 5 | router.post('/reset', async (request, response) => { 6 | await Blog.deleteMany({}) 7 | await User.deleteMany({}) 8 | 9 | response.status(204).end() 10 | }) 11 | 12 | module.exports = router 13 | -------------------------------------------------------------------------------- /part7/bloglist-backend/controllers/users.js: -------------------------------------------------------------------------------- 1 | const bcrypt = require('bcrypt') 2 | const usersRouter = require('express').Router() 3 | const User = require('../models/user') 4 | 5 | usersRouter.get('/', async (request, response) => { 6 | const users = await User 7 | .find({}) 8 | .populate('blogs', { title: 1, author: 1, url: 1 }) 9 | response.json(users.map(u => u.toJSON())) 10 | }) 11 | 12 | usersRouter.post('/', async (request, response) => { 13 | const body = request.body 14 | 15 | if (!body.username || !body.password) { 16 | return response 17 | .status(400) 18 | .json({ error: 'username and password fields are required' }) 19 | } else if (body.username.length <= 3 || body.password.length <= 3) { 20 | return response 21 | .status(400) 22 | .json({ error: 'username and password have to be at least 3 characters long' }) 23 | } 24 | 25 | const saltRounds = 10 26 | const passwordHash = await bcrypt.hash(body.password, saltRounds) 27 | 28 | const user = new User({ 29 | username: body.username, 30 | name: body.name, 31 | passwordHash: passwordHash 32 | }) 33 | 34 | const savedUser = await user.save() 35 | 36 | response.json(savedUser) 37 | }) 38 | 39 | module.exports = usersRouter 40 | -------------------------------------------------------------------------------- /part7/bloglist-backend/index.js: -------------------------------------------------------------------------------- 1 | const app = require('./app') 2 | const config = require('./utils/config') 3 | const logger = require('./utils/logger') 4 | 5 | app.listen(config.PORT, () => { 6 | logger.info(`Server running on port ${config.PORT}`) 7 | }) 8 | -------------------------------------------------------------------------------- /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 | // Set up schema 4 | const blogSchema = mongoose.Schema({ 5 | title: { 6 | type: String, 7 | required: true, 8 | minglength: 5, 9 | }, 10 | author: { 11 | type: String, 12 | required: true, 13 | }, 14 | url: { 15 | type: String, 16 | required: true, 17 | }, 18 | likes: { 19 | type: Number, 20 | required: true, 21 | }, 22 | user: { 23 | type: mongoose.Schema.Types.ObjectId, 24 | ref: 'User', 25 | }, 26 | comments: { 27 | type: [{ type: String }], 28 | }, 29 | }) 30 | 31 | blogSchema.set('toJSON', { 32 | transform: (document, returnedObject) => { 33 | returnedObject.id = returnedObject._id.toString() 34 | delete returnedObject._id 35 | delete returnedObject.__v 36 | }, 37 | }) 38 | 39 | module.exports = mongoose.model('Blog', blogSchema) 40 | -------------------------------------------------------------------------------- /part7/bloglist-backend/models/user.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | const uniqueValidator = require('mongoose-unique-validator') 3 | 4 | const userSchema = new mongoose.Schema({ 5 | username: { 6 | type: String, 7 | unique: true 8 | }, 9 | name: String, 10 | passwordHash: String, 11 | blogs: [ 12 | { 13 | type: mongoose.Schema.Types.ObjectId, 14 | ref: 'Blog' 15 | } 16 | ] 17 | }) 18 | 19 | userSchema.plugin(uniqueValidator) 20 | 21 | userSchema.set('toJSON', { 22 | transform: (document, returnedObject) => { 23 | returnedObject.id = returnedObject._id.toString() 24 | delete returnedObject._id 25 | delete returnedObject.__v 26 | // the passwordHash should not be revealed 27 | delete returnedObject.passwordHash 28 | } 29 | }) 30 | 31 | const User = mongoose.model('User', userSchema) 32 | 33 | module.exports = User 34 | -------------------------------------------------------------------------------- /part7/bloglist-backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bloglist-backend", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "cross-env NODE_ENV=production node index.js", 8 | "dev": "cross-env NODE_ENV=development nodemon index.js", 9 | "test": "cross-env NODE_ENV=test jest --verbose --runInBand", 10 | "lint": "eslint .", 11 | "lint:fix": "eslint . --fix", 12 | "start:test": "cross-env NODE_ENV=test node index.js" 13 | }, 14 | "author": "", 15 | "license": "ISC", 16 | "dependencies": { 17 | "bcrypt": "^4.0.1", 18 | "cors": "^2.8.5", 19 | "dotenv": "^8.2.0", 20 | "express": "^4.17.1", 21 | "express-async-errors": "^3.1.1", 22 | "jsonwebtoken": "^8.5.1", 23 | "mongoose": "^5.9.7", 24 | "mongoose-unique-validator": "^2.0.3" 25 | }, 26 | "devDependencies": { 27 | "cross-env": "^7.0.2", 28 | "eslint": "^6.8.0", 29 | "jest": "^25.2.7", 30 | "nodemon": "^2.0.2", 31 | "supertest": "^4.0.2" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /part7/bloglist-backend/utils/config.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | 3 | let PORT = process.env.PORT 4 | let MONGODB_URI = process.env.MONGODB_URI 5 | 6 | if (process.env.NODE_ENV === 'test') { 7 | MONGODB_URI = process.env.TEST_MONGODB_URI 8 | } 9 | 10 | module.exports = { PORT, MONGODB_URI } 11 | -------------------------------------------------------------------------------- /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, error 13 | } 14 | -------------------------------------------------------------------------------- /part7/bloglist-backend/utils/middleware.js: -------------------------------------------------------------------------------- 1 | // Custom middleware 2 | require('dotenv').config() 3 | const jwt = require('jsonwebtoken') 4 | const logger = require('./logger') 5 | 6 | const requestLogger = (request, response, next) => { 7 | logger.info('Method:', request.method) 8 | logger.info('Path: ', request.path) 9 | logger.info('Body: ', request.body) 10 | logger.info('---') 11 | next() 12 | } 13 | 14 | const unknownEndpoint = (request, response) => { 15 | response.status(404).send({ error: 'unknown endpoint' }) 16 | } 17 | 18 | const errorHandler = (error, request, response, next) => { 19 | logger.error(error.message) 20 | 21 | if (error.name === 'CastError' && error.kind === 'ObjectId') { 22 | return response.status(400).send({ error: 'malformatted id' }) 23 | } else if (error.name === 'ValidationError') { 24 | return response.status(400).json({ error: error.message }) 25 | } else if (error.name === 'JsonWebTokenError') { 26 | return response.status(400).json({ error: 'invalid token' }) 27 | } 28 | 29 | next(error) 30 | } 31 | 32 | // Get user token from request 33 | const tokenExtractor = (request, response, next) => { 34 | const authorization = request.get('authorization') 35 | 36 | // Check if there is a token and extract it 37 | if (authorization && authorization.toLowerCase().startsWith('bearer')) { 38 | request.token = authorization.substring(7) 39 | } else { 40 | request.token = null 41 | } 42 | 43 | // Check if the token is valid 44 | try { 45 | const decodedToken = jwt.verify(request.token, process.env.SECRET) 46 | request.decodedToken = decodedToken 47 | } catch (error) { 48 | request.decodedToken = null 49 | } 50 | 51 | next() 52 | } 53 | 54 | module.exports = { 55 | requestLogger, 56 | unknownEndpoint, 57 | errorHandler, 58 | tokenExtractor, 59 | } 60 | -------------------------------------------------------------------------------- /part7/bloglist-frontend/.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | -------------------------------------------------------------------------------- /part7/bloglist-frontend/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es6: true, 5 | 'jest/globals': true, 6 | 'cypress/globals': true, 7 | }, 8 | extends: ['eslint:recommended', 'plugin:react/recommended'], 9 | parserOptions: { 10 | ecmaFeatures: { 11 | jsx: true, 12 | }, 13 | ecmaVersion: 2018, 14 | sourceType: 'module', 15 | }, 16 | plugins: ['react', 'jest', 'cypress'], 17 | rules: { 18 | indent: ['error', 2, { SwitchCase: 1 }], 19 | 'linebreak-style': ['error', 'unix'], 20 | quotes: ['error', 'single'], 21 | semi: ['error', 'never'], 22 | eqeqeq: 'error', 23 | 'no-trailing-spaces': 'error', 24 | 'object-curly-spacing': ['error', 'always'], 25 | 'arrow-spacing': ['error', { before: true, after: true }], 26 | 'no-console': 0, 27 | 'react/prop-types': 0, 28 | }, 29 | settings: { 30 | react: { 31 | version: 'detect', 32 | }, 33 | }, 34 | } 35 | -------------------------------------------------------------------------------- /part7/bloglist-frontend/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "singleQuote": true, 4 | "semi": false 5 | } 6 | -------------------------------------------------------------------------------- /part7/bloglist-frontend/cypress.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /part7/bloglist-frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bloglist-frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/user-event": "^7.2.1", 7 | "axios": "^0.19.2", 8 | "prop-types": "^15.7.2", 9 | "react": "^16.12.0", 10 | "react-dom": "^16.12.0", 11 | "react-redux": "^7.2.1", 12 | "react-router-dom": "^5.2.0", 13 | "react-scripts": "^3.4.3", 14 | "redux": "^4.0.5", 15 | "redux-thunk": "^2.3.0", 16 | "styled-components": "^5.2.0" 17 | }, 18 | "scripts": { 19 | "start": "react-scripts start", 20 | "build": "react-scripts build", 21 | "test": "react-scripts test", 22 | "eject": "react-scripts eject", 23 | "lint": "eslint .", 24 | "lint:fix": "eslint . --fix", 25 | "cypress:open": "cypress open", 26 | "test:e2e": "cypress run" 27 | }, 28 | "eslintConfig": { 29 | "extends": "react-app" 30 | }, 31 | "browserslist": { 32 | "production": [ 33 | ">0.2%", 34 | "not dead", 35 | "not op_mini all" 36 | ], 37 | "development": [ 38 | "last 1 chrome version", 39 | "last 1 firefox version", 40 | "last 1 safari version" 41 | ] 42 | }, 43 | "proxy": "http://localhost:3001", 44 | "devDependencies": { 45 | "@testing-library/jest-dom": "^4.2.4", 46 | "@testing-library/react": "^9.5.0", 47 | "cypress": "^4.4.1", 48 | "eslint-plugin-cypress": "^2.11.2", 49 | "eslint-plugin-jest": "^23.8.2", 50 | "eslint-plugin-react": "^7.21.4" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /part7/bloglist-frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orrsteinberg/fullstackopen-2020/2565b48898ac7249e645ba3ba96fdef4780b782a/part7/bloglist-frontend/public/favicon.ico -------------------------------------------------------------------------------- /part7/bloglist-frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /part7/bloglist-frontend/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orrsteinberg/fullstackopen-2020/2565b48898ac7249e645ba3ba96fdef4780b782a/part7/bloglist-frontend/public/logo192.png -------------------------------------------------------------------------------- /part7/bloglist-frontend/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orrsteinberg/fullstackopen-2020/2565b48898ac7249e645ba3ba96fdef4780b782a/part7/bloglist-frontend/public/logo512.png -------------------------------------------------------------------------------- /part7/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 | "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/bloglist-frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /part7/bloglist-frontend/src/components/Blogs.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useSelector, useDispatch } from 'react-redux' 3 | import { Link } from 'react-router-dom' 4 | 5 | import { createBlog } from '../reducers/blogReducer' 6 | import { Container, PageHeader, PageTitle, List, ListItem } from '../globalStyles' 7 | import Togglable from './Togglable' 8 | import CreateBlogForm from './CreateBlogForm' 9 | 10 | const Blogs = () => { 11 | const blogs = useSelector((state) => state.blogs.sort((a, b) => b.likes - a.likes)) 12 | const dispatch = useDispatch() 13 | // Ref toggleable component to access its toggle function on new blog creation 14 | const blogFormRef = React.useRef() 15 | 16 | const handleCreateBlog = ({ title, author, url }) => { 17 | dispatch(createBlog({ title, author, url })) 18 | blogFormRef.current.toggleVisibility() 19 | } 20 | 21 | return ( 22 | 23 | 24 | Blogs 25 | 26 | 27 | 28 | 29 | 30 | {blogs.map(({ id, title, author }) => ( 31 | 32 | 33 | {title} by {author} 34 | 35 | 36 | ))} 37 | 38 | 39 | ) 40 | } 41 | 42 | export default Blogs 43 | -------------------------------------------------------------------------------- /part7/bloglist-frontend/src/components/CreateBlogForm.js: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { useField } from '../hooks' 4 | import { Button, Input } from '../globalStyles' 5 | 6 | const CreateBlogForm = ({ createBlog }) => { 7 | const [titleField, clearTitleField] = useField('text') 8 | const [authorField, clearAuthorField] = useField('text') 9 | const [urlField, clearUrlField] = useField('text') 10 | 11 | const clearAllFields = useCallback(() => { 12 | clearTitleField() 13 | clearAuthorField() 14 | clearUrlField() 15 | }, []) 16 | 17 | const handleSubmit = (event) => { 18 | event.preventDefault() 19 | createBlog({ 20 | title: titleField.value, 21 | author: authorField.value, 22 | url: urlField.value, 23 | }) 24 | clearAllFields() 25 | } 26 | 27 | return ( 28 | <> 29 |

Create new

30 |
31 |
32 | Title: 33 |
34 |
35 | Author: 36 |
37 |
38 | URL: 39 |
40 | 43 |
44 | 45 | ) 46 | } 47 | 48 | CreateBlogForm.propTypes = { 49 | createBlog: PropTypes.func.isRequired, 50 | } 51 | 52 | export default CreateBlogForm 53 | -------------------------------------------------------------------------------- /part7/bloglist-frontend/src/components/LoginForm.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useDispatch } from 'react-redux' 3 | import { useField } from '../hooks' 4 | import { loginUser } from '../reducers/currentUserReducer' 5 | import { Container, PageHeader, PageTitle, Card, Button, Input } from '../globalStyles' 6 | import Notification from './Notification' 7 | 8 | const LoginForm = () => { 9 | const [usernameField] = useField('text') 10 | const [passwordField] = useField('password') 11 | const dispatch = useDispatch() 12 | 13 | const handleLogin = (event) => { 14 | event.preventDefault() 15 | dispatch(loginUser({ username: usernameField.value, password: passwordField.value })) 16 | } 17 | 18 | return ( 19 | 20 | 21 | Log In 22 | 23 | 24 | 25 |
26 |
27 | Username: 28 |
29 |
30 | Password: 31 |
32 | 35 |
36 |
37 |
38 | ) 39 | } 40 | 41 | export default LoginForm 42 | -------------------------------------------------------------------------------- /part7/bloglist-frontend/src/components/MainHeader.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { Link } from 'react-router-dom' 4 | import styled from 'styled-components' 5 | import { Button } from '../globalStyles' 6 | 7 | const MainHeaderWrapper = styled.header` 8 | background: #fff; 9 | display: flex; 10 | flex-wrap: wrap; 11 | align-items: center; 12 | justify-content: space-around; 13 | ` 14 | 15 | const MainHeaderNav = styled.nav` 16 | & a { 17 | font-size: 1.3rem; 18 | color: #384ebf; 19 | margin-left: 1rem; 20 | display: inline-block; 21 | padding: 1rem; 22 | text-decoration: none; 23 | 24 | &:hover { 25 | background: #384ebf; 26 | color: #fff; 27 | } 28 | } 29 | ` 30 | 31 | const Logo = styled.h2` 32 | color: #f21f82; 33 | letter-spacing: -1px; 34 | ` 35 | 36 | const MainHeader = ({ currentUserName, handleLogout }) => ( 37 | 38 | Blogslist App 39 | 40 | Blogs 41 | Users 42 | 43 |
{currentUserName} logged in
44 | 45 |
46 | ) 47 | 48 | MainHeader.propTypes = { 49 | currentUserName: PropTypes.string.isRequired, 50 | handleLogout: PropTypes.func.isRequired, 51 | } 52 | 53 | export default MainHeader 54 | -------------------------------------------------------------------------------- /part7/bloglist-frontend/src/components/Notification.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useSelector } from 'react-redux' 3 | import styled from 'styled-components' 4 | 5 | const Alert = styled.div` 6 | padding: 1rem; 7 | margin: 2rem auto; 8 | max-width: 600px; 9 | font-size: 1.3rem; 10 | text-align: center; 11 | background: ${(props) => (props.type === 'success' ? '#22c634' : '#e6172b')}; 12 | color: #fff; 13 | border-radius: 5px; 14 | ` 15 | 16 | const Notification = () => { 17 | const { message, notificationType } = useSelector((state) => state.notification) 18 | 19 | if (!message) { 20 | return null 21 | } 22 | 23 | return {message} 24 | } 25 | 26 | export default Notification 27 | -------------------------------------------------------------------------------- /part7/bloglist-frontend/src/components/Togglable.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useImperativeHandle } from 'react' 2 | import styled, { css } from 'styled-components' 3 | import PropTypes from 'prop-types' 4 | import { Card, Button } from '../globalStyles' 5 | 6 | const StyledToggleableDiv = styled.div` 7 | ${({ hide }) => 8 | hide && 9 | css` 10 | display: none; 11 | `} 12 | ` 13 | 14 | const Togglable = React.forwardRef(({ buttonLabel, children }, ref) => { 15 | const [isVisible, setIsVisible] = useState(false) 16 | 17 | const toggleVisibility = () => setIsVisible(!isVisible) 18 | 19 | useImperativeHandle(ref, () => { 20 | return { 21 | toggleVisibility, 22 | } 23 | }) 24 | 25 | return ( 26 | 27 | 28 | 31 | 32 | 33 | {children} 34 | 35 | 36 | 37 | ) 38 | }) 39 | 40 | Togglable.propTypes = { 41 | buttonLabel: PropTypes.string.isRequired, 42 | children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]).isRequired, 43 | } 44 | 45 | Togglable.displayName = 'Toggleable' 46 | 47 | export default Togglable 48 | -------------------------------------------------------------------------------- /part7/bloglist-frontend/src/components/User.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useSelector } from 'react-redux' 3 | import { Link, Redirect, useRouteMatch } from 'react-router-dom' 4 | import { Container, Section, SectionTitle, List, ListItem } from '../globalStyles' 5 | 6 | const UserPage = () => { 7 | const { 8 | params: { id: userIdMatch }, 9 | } = useRouteMatch('/users/:id') 10 | const userToView = useSelector((state) => state.users.find((u) => u.id === userIdMatch)) 11 | 12 | if (!userToView) { 13 | return 14 | } 15 | 16 | const { name, blogs } = userToView 17 | 18 | return ( 19 | 20 |
21 | 22 | {name} 23 | 24 | Added blogs: 25 | 26 | {blogs.map(({ id, title }) => ( 27 | 28 | {title} 29 | 30 | ))} 31 | 32 |
33 |
34 | ) 35 | } 36 | 37 | export default UserPage 38 | -------------------------------------------------------------------------------- /part7/bloglist-frontend/src/components/Users.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useSelector } from 'react-redux' 3 | import { Link } from 'react-router-dom' 4 | import { Container, PageHeader, PageTitle, TableCentered } from '../globalStyles' 5 | 6 | const Users = () => { 7 | const users = useSelector((state) => state.users) 8 | 9 | return ( 10 | 11 | 12 | Users 13 | 14 | 15 | 16 | 17 | 18 | Blogs Created 19 | 20 | 21 | 22 | {users.map(({ id, name, blogs }) => ( 23 | 24 | 25 | {name} 26 | 27 | {blogs.length} 28 | 29 | ))} 30 | 31 | 32 | 33 | ) 34 | } 35 | 36 | export default Users 37 | -------------------------------------------------------------------------------- /part7/bloglist-frontend/src/hooks/index.js: -------------------------------------------------------------------------------- 1 | import { useState, useCallback } from 'react' 2 | 3 | export const useField = (type) => { 4 | const [value, setValue] = useState('') 5 | 6 | const onChange = useCallback((event) => setValue(event.target.value), []) 7 | 8 | const clear = useCallback(() => setValue(''), []) 9 | 10 | return [ 11 | { 12 | type, 13 | value, 14 | onChange, 15 | }, 16 | clear, 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /part7/bloglist-frontend/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 | .error { 11 | color: red; 12 | background: lightgrey; 13 | font-size: 20px; 14 | border-style: solid; 15 | border-radius: 5px; 16 | padding: 10px; 17 | margin-bottom: 10px; 18 | } 19 | 20 | .success { 21 | color: green; 22 | background: lightgrey; 23 | font-size: 20px; 24 | border-style: solid; 25 | border-radius: 5px; 26 | padding: 10px; 27 | margin-bottom: 10px; 28 | } 29 | -------------------------------------------------------------------------------- /part7/bloglist-frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import { Provider } from 'react-redux' 4 | import { BrowserRouter as Router } from 'react-router-dom' 5 | import store from './store' 6 | import { GlobalStyle } from './globalStyles' 7 | import App from './App' 8 | 9 | ReactDOM.render( 10 | 11 | 12 | 13 | 14 | 15 | , 16 | document.getElementById('root') 17 | ) 18 | -------------------------------------------------------------------------------- /part7/bloglist-frontend/src/reducers/currentUserReducer.js: -------------------------------------------------------------------------------- 1 | import loginService from '../services/login' 2 | import blogService from '../services/blogs' 3 | import { setNotification } from './notificationReducer' 4 | 5 | export const checkCurrentUser = () => { 6 | return (dispatch) => { 7 | const loggedInUserJSON = window.localStorage.getItem('loggedInUser') 8 | 9 | if (loggedInUserJSON) { 10 | const user = JSON.parse(loggedInUserJSON) 11 | dispatch(setCurrentUser(user)) 12 | } 13 | } 14 | } 15 | 16 | export const loginUser = ({ username, password }) => { 17 | return async (dispatch) => { 18 | try { 19 | const user = await loginService.login({ username, password }) 20 | // Set auth token and localStorage info 21 | blogService.setToken(user.token) 22 | window.localStorage.setItem('loggedInUser', JSON.stringify(user)) 23 | dispatch(setCurrentUser(user)) 24 | } catch (error) { 25 | dispatch(setNotification('Wrong username or password', 'error')) 26 | } 27 | } 28 | } 29 | 30 | export const setCurrentUser = (user) => { 31 | return { 32 | type: 'SET_CURRENT_USER', 33 | data: user, 34 | } 35 | } 36 | 37 | export const clearCurrentUser = () => { 38 | return { type: 'CLEAR_CURRENT_USER' } 39 | } 40 | 41 | const currentUserReducer = (state = null, action) => { 42 | switch (action.type) { 43 | case 'SET_CURRENT_USER': 44 | return action.data 45 | case 'CLEAR_CURRENT_USER': 46 | return null 47 | default: 48 | return state 49 | } 50 | } 51 | 52 | export default currentUserReducer 53 | -------------------------------------------------------------------------------- /part7/bloglist-frontend/src/reducers/notificationReducer.js: -------------------------------------------------------------------------------- 1 | export const setNotification = (message, notificationType) => { 2 | // Saving timeout id on the global window object so it can be cleared directly 3 | if (window._bloglistNotificationTimeout) { 4 | window.clearTimeout(window._bloglistNotificationTimeout) 5 | } 6 | 7 | return async (dispatch) => { 8 | dispatch({ type: 'DISPLAY_NOTIFICATION', data: { message, notificationType } }) 9 | 10 | window._bloglistNotificationTimeout = setTimeout( 11 | () => dispatch({ type: 'CLEAR_NOTIFICATION' }), 12 | 5000 13 | ) 14 | } 15 | } 16 | 17 | const initialState = { 18 | message: null, 19 | notificationType: null, 20 | } 21 | 22 | const notificationReducer = (state = initialState, action) => { 23 | switch (action.type) { 24 | case 'DISPLAY_NOTIFICATION': 25 | return action.data 26 | case 'CLEAR_NOTIFICATION': 27 | return { 28 | message: null, 29 | notificationType: null, 30 | } 31 | 32 | default: 33 | return state 34 | } 35 | } 36 | 37 | export default notificationReducer 38 | -------------------------------------------------------------------------------- /part7/bloglist-frontend/src/reducers/userReducer.js: -------------------------------------------------------------------------------- 1 | import userService from '../services/users' 2 | import { setNotification } from './notificationReducer' 3 | 4 | export const initializeUsers = () => { 5 | return async (dispatch) => { 6 | try { 7 | const users = await userService.getAll() 8 | dispatch({ type: 'INIT_USERS', data: users }) 9 | } catch (error) { 10 | setNotification('Error fetching list of users', 'error') 11 | } 12 | } 13 | } 14 | 15 | const userReducer = (state = [], action) => { 16 | switch (action.type) { 17 | case 'INIT_USERS': 18 | return action.data 19 | default: 20 | return state 21 | } 22 | } 23 | 24 | export default userReducer 25 | -------------------------------------------------------------------------------- /part7/bloglist-frontend/src/services/blogs.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | const baseUrl = '/api/blogs' 3 | 4 | let token = null 5 | 6 | const createAuthConfig = (token) => ({ 7 | headers: { 8 | Authorization: token, 9 | }, 10 | }) 11 | 12 | const setToken = (newToken) => { 13 | token = `bearer ${newToken}` 14 | } 15 | 16 | const clearToken = () => (token = null) 17 | 18 | const getAll = async () => { 19 | const response = await axios.get(baseUrl) 20 | return response.data 21 | } 22 | 23 | const create = async (blogObject) => { 24 | const response = await axios.post(baseUrl, blogObject, createAuthConfig(token)) 25 | return response.data 26 | } 27 | 28 | const update = async (blogObject) => { 29 | const response = await axios.put( 30 | `${baseUrl}/${blogObject.id}`, 31 | blogObject, 32 | createAuthConfig(token) 33 | ) 34 | return response.data 35 | } 36 | 37 | const comment = async (blogObject) => { 38 | const response = await axios.post( 39 | `${baseUrl}/${blogObject.id}/comments`, 40 | blogObject, 41 | createAuthConfig(token) 42 | ) 43 | return response.data 44 | } 45 | 46 | const remove = async (blogId) => { 47 | return await axios.delete(`${baseUrl}/${blogId}`, createAuthConfig(token)) 48 | } 49 | 50 | export default { getAll, create, update, remove, comment, setToken, clearToken } 51 | -------------------------------------------------------------------------------- /part7/bloglist-frontend/src/services/login.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | const baseUrl = '/api/login' 3 | 4 | const login = async (credentials) => { 5 | const response = await axios.post(baseUrl, credentials) 6 | return response.data 7 | } 8 | 9 | export default { login } 10 | -------------------------------------------------------------------------------- /part7/bloglist-frontend/src/services/users.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | const baseUrl = 'api/users' 4 | 5 | const getAll = async () => { 6 | const response = await axios.get(baseUrl) 7 | return response.data 8 | } 9 | 10 | const getUser = async (id) => { 11 | const response = await axios.get(`${baseUrl}/${id}`) 12 | return response.data 13 | } 14 | 15 | export default { getAll, getUser } 16 | -------------------------------------------------------------------------------- /part7/bloglist-frontend/src/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, combineReducers, applyMiddleware } from 'redux' 2 | import thunk from 'redux-thunk' 3 | import blogReducer from './reducers/blogReducer' 4 | import userReducer from './reducers/userReducer' 5 | import currentUserReducer from './reducers/currentUserReducer' 6 | import notificationReducer from './reducers/notificationReducer' 7 | 8 | const rootReducer = combineReducers({ 9 | blogs: blogReducer, 10 | users: userReducer, 11 | currentUser: currentUserReducer, 12 | notification: notificationReducer, 13 | }) 14 | 15 | const store = createStore(rootReducer, applyMiddleware(thunk)) 16 | 17 | export default store 18 | -------------------------------------------------------------------------------- /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/orrsteinberg/fullstackopen-2020/2565b48898ac7249e645ba3ba96fdef4780b782a/part7/country-hook/public/favicon.ico -------------------------------------------------------------------------------- /part7/country-hook/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /part7/country-hook/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orrsteinberg/fullstackopen-2020/2565b48898ac7249e645ba3ba96fdef4780b782a/part7/country-hook/public/logo192.png -------------------------------------------------------------------------------- /part7/country-hook/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orrsteinberg/fullstackopen-2020/2565b48898ac7249e645ba3ba96fdef4780b782a/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/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "routed-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-router-dom": "^5.2.0", 12 | "react-scripts": "3.3.1" 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/routed-anecdotes/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orrsteinberg/fullstackopen-2020/2565b48898ac7249e645ba3ba96fdef4780b782a/part7/routed-anecdotes/public/favicon.ico -------------------------------------------------------------------------------- /part7/routed-anecdotes/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /part7/routed-anecdotes/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orrsteinberg/fullstackopen-2020/2565b48898ac7249e645ba3ba96fdef4780b782a/part7/routed-anecdotes/public/logo192.png -------------------------------------------------------------------------------- /part7/routed-anecdotes/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orrsteinberg/fullstackopen-2020/2565b48898ac7249e645ba3ba96fdef4780b782a/part7/routed-anecdotes/public/logo512.png -------------------------------------------------------------------------------- /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 | "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/routed-anecdotes/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /part7/routed-anecdotes/src/hooks/index.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | 3 | export const useField = (type) => { 4 | const [value, setValue] = useState(""); 5 | 6 | const onChange = (event) => { 7 | setValue(event.target.value); 8 | }; 9 | 10 | const reset = () => 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/db.json: -------------------------------------------------------------------------------- 1 | { 2 | "notes": [ 3 | { 4 | "content": "custom-hookit aivan mahtavia", 5 | "id": 1 6 | }, 7 | { 8 | "content": "paras feature ikinä <3", 9 | "id": 2 10 | } 11 | ], 12 | "persons": [ 13 | { 14 | "name": "mluukkai", 15 | "number": "040-5483923", 16 | "id": 1 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /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.3.1" 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/orrsteinberg/fullstackopen-2020/2565b48898ac7249e645ba3ba96fdef4780b782a/part7/ultimate-hooks/public/favicon.ico -------------------------------------------------------------------------------- /part7/ultimate-hooks/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /part7/ultimate-hooks/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orrsteinberg/fullstackopen-2020/2565b48898ac7249e645ba3ba96fdef4780b782a/part7/ultimate-hooks/public/logo192.png -------------------------------------------------------------------------------- /part7/ultimate-hooks/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orrsteinberg/fullstackopen-2020/2565b48898ac7249e645ba3ba96fdef4780b782a/part7/ultimate-hooks/public/logo512.png -------------------------------------------------------------------------------- /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 | "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/ultimate-hooks/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /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/library-backend/index.js: -------------------------------------------------------------------------------- 1 | const { ApolloServer } = require("apollo-server"); 2 | const mongoose = require("mongoose"); 3 | const jwt = require("jsonwebtoken"); 4 | const User = require("./models/User"); 5 | const typeDefs = require("./typeDefs"); 6 | const resolvers = require("./resolvers"); 7 | 8 | require("dotenv").config(); 9 | const MONGODB_URI = process.env.MONGODB_URI; 10 | const JWT_SECRET = process.env.JWT_SECRET; 11 | 12 | console.log("Connecting to MongoDB"); 13 | 14 | mongoose 15 | .connect(MONGODB_URI, { 16 | useNewUrlParser: true, 17 | useUnifiedTopology: true, 18 | useFindAndModify: false, 19 | }) 20 | .then(() => { 21 | console.log("Connected to MongoDB"); 22 | }) 23 | .catch((err) => { 24 | console.log("Error connecting to MongoDB", err.message); 25 | }); 26 | 27 | const server = new ApolloServer({ 28 | typeDefs, 29 | resolvers, 30 | context: async ({ req }) => { 31 | const auth = req ? req.headers.authorization : null; 32 | if (auth && auth.toLowerCase().startsWith("bearer")) { 33 | const decoded = jwt.verify(auth.substring(7), JWT_SECRET); 34 | const currentUser = await User.findById(decoded.id); 35 | return { currentUser }; 36 | } 37 | }, 38 | }); 39 | 40 | server.listen().then(({ url, subscriptionsUrl }) => { 41 | console.log(`Server ready at ${url}`); 42 | console.log(`Subscriptions ready at ${subscriptionsUrl}`); 43 | }); 44 | -------------------------------------------------------------------------------- /part8/library-backend/models/Author.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const schema = new mongoose.Schema({ 4 | name: { 5 | type: String, 6 | required: true, 7 | unique: true, 8 | minlength: 4, 9 | }, 10 | born: { 11 | type: Number, 12 | }, 13 | books: [ 14 | { 15 | type: mongoose.Schema.Types.ObjectId, 16 | ref: "Book", 17 | }, 18 | ], 19 | }); 20 | 21 | module.exports = mongoose.model("Author", schema); 22 | -------------------------------------------------------------------------------- /part8/library-backend/models/Book.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const schema = new mongoose.Schema({ 4 | title: { 5 | type: String, 6 | required: true, 7 | unique: true, 8 | minlength: 2, 9 | }, 10 | published: { 11 | type: Number, 12 | }, 13 | author: { 14 | type: mongoose.Schema.Types.ObjectId, 15 | ref: "Author", 16 | }, 17 | genres: [{ type: String }], 18 | }); 19 | 20 | module.exports = mongoose.model("Book", schema); 21 | -------------------------------------------------------------------------------- /part8/library-backend/models/User.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const schema = new mongoose.Schema({ 4 | username: { 5 | type: String, 6 | required: true, 7 | unique: true, 8 | minlength: 3, 9 | }, 10 | favoriteGenre: { 11 | type: String, 12 | required: true, 13 | }, 14 | }); 15 | 16 | module.exports = mongoose.model("User", schema); 17 | -------------------------------------------------------------------------------- /part8/library-backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "library-backend", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "nodemon index.js" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "nodemon": "^2.0.4" 14 | }, 15 | "dependencies": { 16 | "apollo-server": "^2.16.0", 17 | "dotenv": "^8.2.0", 18 | "graphql": "^15.3.0", 19 | "jsonwebtoken": "^8.5.1", 20 | "mongoose": "^5.9.27" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /part8/library-backend/typeDefs.js: -------------------------------------------------------------------------------- 1 | const { gql } = require("apollo-server") 2 | 3 | module.exports = gql` 4 | type Book { 5 | title: String! 6 | published: Int! 7 | author: Author! 8 | genres: [String!] 9 | id: ID! 10 | } 11 | 12 | type Author { 13 | name: String! 14 | born: Int 15 | bookCount: Int! 16 | id: ID! 17 | } 18 | 19 | type User { 20 | username: String! 21 | favoriteGenre: String! 22 | id: ID! 23 | } 24 | 25 | type Token { 26 | value: String! 27 | } 28 | 29 | type Query { 30 | bookCount: Int! 31 | authorCount: Int! 32 | allBooks(author: String, genre: String): [Book!]! 33 | allAuthors: [Author!]! 34 | me: User 35 | } 36 | 37 | type Mutation { 38 | addBook( 39 | title: String! 40 | published: Int! 41 | author: String! 42 | genres: [String!]! 43 | ): Book 44 | editAuthor(name: String!, setBornTo: Int!): Author 45 | createUser(username: String!, favoriteGenre: String!): User 46 | login(username: String!, password: String!): Token 47 | } 48 | 49 | type Subscription { 50 | bookAdded: Book! 51 | } 52 | `; 53 | 54 | -------------------------------------------------------------------------------- /part8/library-frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "library-frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@apollo/client": "^3.2.4", 7 | "@testing-library/jest-dom": "^4.2.4", 8 | "@testing-library/react": "^9.4.1", 9 | "@testing-library/user-event": "^7.2.1", 10 | "apollo-link-context": "^1.0.20", 11 | "graphql": "^15.3.0", 12 | "react": "^16.12.0", 13 | "react-dom": "^16.12.0", 14 | "react-scripts": "3.4.0", 15 | "react-select": "^3.1.0", 16 | "subscriptions-transport-ws": "^0.9.18" 17 | }, 18 | "scripts": { 19 | "start": "react-scripts start", 20 | "build": "react-scripts build", 21 | "test": "react-scripts test", 22 | "eject": "react-scripts eject" 23 | }, 24 | "eslintConfig": { 25 | "extends": "react-app" 26 | }, 27 | "browserslist": { 28 | "production": [ 29 | ">0.2%", 30 | "not dead", 31 | "not op_mini all" 32 | ], 33 | "development": [ 34 | "last 1 chrome version", 35 | "last 1 firefox version", 36 | "last 1 safari version" 37 | ] 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /part8/library-frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orrsteinberg/fullstackopen-2020/2565b48898ac7249e645ba3ba96fdef4780b782a/part8/library-frontend/public/favicon.ico -------------------------------------------------------------------------------- /part8/library-frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /part8/library-frontend/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orrsteinberg/fullstackopen-2020/2565b48898ac7249e645ba3ba96fdef4780b782a/part8/library-frontend/public/logo192.png -------------------------------------------------------------------------------- /part8/library-frontend/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orrsteinberg/fullstackopen-2020/2565b48898ac7249e645ba3ba96fdef4780b782a/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/Authors.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { useQuery } from "@apollo/client"; 3 | import { ALL_AUTHORS } from "../queries"; 4 | import SetBirthYear from "./SetBirthYear" 5 | 6 | const Authors = (props) => { 7 | const [authors, setAuthors] = useState([]); 8 | const { loading, error, data } = useQuery(ALL_AUTHORS); 9 | 10 | useEffect(() => { 11 | if (data) { 12 | setAuthors(data.allAuthors); 13 | } 14 | }, [setAuthors, data]); 15 | 16 | // Return appropriate render 17 | if (!props.show) return null; 18 | 19 | if (loading) return
Loading...
; 20 | 21 | if (error) { 22 | console.error(error); 23 | return
Oops! Something went wrong
; 24 | } 25 | 26 | return ( 27 |
28 |

Authors

29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | {authors.map((a) => ( 37 | 38 | 39 | 40 | 41 | 42 | ))} 43 | 44 |
BornBooks
{a.name}{a.born}{a.bookCount}
45 | 46 |
47 | ); 48 | }; 49 | 50 | export default Authors; 51 | -------------------------------------------------------------------------------- /part8/library-frontend/src/components/LoginForm.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { useMutation } from "@apollo/client"; 3 | import { LOGIN } from "../queries"; 4 | 5 | const LoginForm = ({ setError, setToken, redirect, show, isLoggedIn }) => { 6 | const [username, setUsername] = useState(""); 7 | const [password, setPassword] = useState(""); 8 | 9 | const [login, result] = useMutation(LOGIN, { 10 | onError: (error) => { 11 | setError(error.graphQLErrors[0].message); 12 | }, 13 | }); 14 | 15 | useEffect(() => { 16 | if (result.data) { 17 | const token = result.data.login.value; 18 | setToken(token); 19 | localStorage.setItem("library-user-token", token); 20 | } 21 | }, [result.data, setToken]); 22 | 23 | if (!show) { 24 | return null; 25 | } else if (isLoggedIn) { 26 | redirect("authors"); 27 | } 28 | 29 | const submit = (event) => { 30 | event.preventDefault(); 31 | 32 | login({ variables: { username, password } }); 33 | 34 | setUsername(""); 35 | setPassword(""); 36 | }; 37 | 38 | return ( 39 |
40 |

Login

41 |
42 |
43 | setUsername(target.value)} 47 | /> 48 |
49 |
50 | setPassword(target.value)} 54 | /> 55 |
56 | 57 |
58 |
59 | ); 60 | }; 61 | 62 | export default LoginForm; 63 | -------------------------------------------------------------------------------- /part8/library-frontend/src/components/Notification.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Notification = ({ errorMessage }) => { 4 | if (!errorMessage) { 5 | return null; 6 | } 7 | return ( 8 |
9 | {errorMessage} 10 |
11 | ); 12 | }; 13 | 14 | export default Notification; 15 | -------------------------------------------------------------------------------- /part8/library-frontend/src/components/Recommend.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { useQuery, useLazyQuery } from "@apollo/client"; 3 | import { ALL_BOOKS, ME } from "../queries"; 4 | 5 | const Recommend = (props) => { 6 | const [books, setBooks] = useState([]); 7 | const [getAllBooks, allBooks] = useLazyQuery(ALL_BOOKS); 8 | const user = useQuery(ME); 9 | 10 | useEffect(() => { 11 | if (user.data && user.data.me) { 12 | getAllBooks(); 13 | } 14 | }, [user.data, getAllBooks]); 15 | 16 | useEffect(() => { 17 | if (allBooks.data) { 18 | setBooks(allBooks.data.allBooks); 19 | } 20 | }, [allBooks.data, setBooks]); 21 | 22 | // Return appropriate render 23 | if (!props.show) return null; 24 | 25 | if (books.loading) return
Loading...
; 26 | 27 | if (user.error || books.error) { 28 | return
Oops! Something went wrong
; 29 | } 30 | 31 | const recommendedBooks = books.filter((b) => 32 | b.genres.includes(user.data.me.favoriteGenre) 33 | ); 34 | 35 | return ( 36 |
37 |

Recommended books

38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | {recommendedBooks.map((a) => ( 46 | 47 | 48 | 49 | 50 | 51 | ))} 52 | 53 |
AuthorPublished
{a.title}{a.author.name}{a.published}
54 |
55 | ); 56 | }; 57 | 58 | export default Recommend; 59 | -------------------------------------------------------------------------------- /part8/library-frontend/src/components/SetBirthYear.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { useMutation } from "@apollo/client"; 3 | import { ALL_AUTHORS, EDIT_AUTHOR } from "../queries"; 4 | import Select from "react-select"; 5 | 6 | const SetBirthYear = ({ authors, setError }) => { 7 | const [selectedAuthor, setSelectedAuthor] = useState(null); 8 | const [born, setBorn] = useState(""); 9 | 10 | const [editAuthor] = useMutation(EDIT_AUTHOR, { 11 | refetchQueries: [{ query: ALL_AUTHORS }], 12 | onError: (error) => setError(error.message), 13 | }); 14 | 15 | const submit = (event) => { 16 | event.preventDefault(); 17 | 18 | editAuthor({ 19 | variables: { name: selectedAuthor.value, setBornTo: born }, 20 | }); 21 | 22 | setSelectedAuthor(null); 23 | setBorn(""); 24 | }; 25 | 26 | const options = authors.map((a) => { 27 | return { value: a.name, label: a.name }; 28 | }); 29 | 30 | return ( 31 |
32 |

Set Birthyear

33 |
34 | setBorn(parseInt(target.value))} 44 | value={born} 45 | /> 46 |
47 | 48 | 49 | 50 | ); 51 | }; 52 | 53 | export default SetBirthYear; 54 | -------------------------------------------------------------------------------- /part8/library-frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import { setContext } from "apollo-link-context"; 4 | import { 5 | ApolloClient, 6 | ApolloProvider, 7 | HttpLink, 8 | InMemoryCache, 9 | split, 10 | } from "@apollo/client"; 11 | import { getMainDefinition } from "@apollo/client/utilities"; 12 | import { WebSocketLink } from "@apollo/client/link/ws"; 13 | import App from "./App"; 14 | 15 | const authLinkContext = setContext((_, { headers }) => { 16 | const token = localStorage.getItem("library-user-token"); 17 | return { 18 | headers: { 19 | ...headers, 20 | authorization: token ? `bearer ${token}` : null, 21 | }, 22 | }; 23 | }); 24 | 25 | // Initialize regular HTTP link 26 | const httpLink = new HttpLink({ uri: "http://localhost:4000" }); 27 | 28 | // Initialize a WebSocket link 29 | const wsLink = new WebSocketLink({ 30 | uri: `ws://localhost:4000/graphql`, 31 | options: { 32 | reconnect: true, 33 | }, 34 | }); 35 | 36 | // Allow the client to use different links according to a result of a boolean function 37 | // (subscriptions with ws, queries and mutations with http) 38 | const splitLink = split( 39 | ({ query }) => { 40 | const definition = getMainDefinition(query); 41 | return ( 42 | definition.kind === "OperationDefinition" && 43 | definition.operation === "subscription" 44 | ); 45 | }, 46 | wsLink, 47 | authLinkContext.concat(httpLink) 48 | ); 49 | 50 | const client = new ApolloClient({ 51 | cache: new InMemoryCache(), 52 | link: splitLink, 53 | }); 54 | 55 | ReactDOM.render( 56 | 57 | 58 | , 59 | document.getElementById("root") 60 | ); 61 | -------------------------------------------------------------------------------- /part9/courseinfo_ts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "halfstack-typescript", 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.55", 11 | "@types/react": "^16.9.49", 12 | "@types/react-dom": "^16.9.8", 13 | "react": "^16.13.1", 14 | "react-dom": "^16.13.1", 15 | "react-scripts": "3.4.3", 16 | "typescript": "^3.7.5" 17 | }, 18 | "scripts": { 19 | "start": "react-scripts start", 20 | "build": "react-scripts build", 21 | "test": "react-scripts test", 22 | "eject": "react-scripts eject" 23 | }, 24 | "eslintConfig": { 25 | "extends": "react-app" 26 | }, 27 | "browserslist": { 28 | "production": [ 29 | ">0.2%", 30 | "not dead", 31 | "not op_mini all" 32 | ], 33 | "development": [ 34 | "last 1 chrome version", 35 | "last 1 firefox version", 36 | "last 1 safari version" 37 | ] 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /part9/courseinfo_ts/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orrsteinberg/fullstackopen-2020/2565b48898ac7249e645ba3ba96fdef4780b782a/part9/courseinfo_ts/public/favicon.ico -------------------------------------------------------------------------------- /part9/courseinfo_ts/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /part9/courseinfo_ts/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orrsteinberg/fullstackopen-2020/2565b48898ac7249e645ba3ba96fdef4780b782a/part9/courseinfo_ts/public/logo192.png -------------------------------------------------------------------------------- /part9/courseinfo_ts/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orrsteinberg/fullstackopen-2020/2565b48898ac7249e645ba3ba96fdef4780b782a/part9/courseinfo_ts/public/logo512.png -------------------------------------------------------------------------------- /part9/courseinfo_ts/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/courseinfo_ts/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /part9/courseinfo_ts/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { CoursePart } from "./types"; 3 | import Header from "./Header"; 4 | import Content from "./Content"; 5 | import Total from "./Total"; 6 | 7 | const App: React.FC = () => { 8 | const courseName = "Half Stack application development"; 9 | const courseParts: CoursePart[] = [ 10 | { 11 | name: "Fundamentals", 12 | exerciseCount: 10, 13 | description: "This is an awesome course part" 14 | }, 15 | { 16 | name: "Using props to pass data", 17 | exerciseCount: 7, 18 | groupProjectCount: 3 19 | }, 20 | { 21 | name: "Deeper type usage", 22 | exerciseCount: 14, 23 | description: "Confusing description", 24 | exerciseSubmissionLink: "https://fake-exercise-submit.made-up-url.dev" 25 | }, 26 | { 27 | name: "New course part interface", 28 | exerciseCount: 10, 29 | description: "Some description", 30 | groupProjectCount: 2, 31 | exerciseSubmissionLink: "https://fake-exercise-submit.made-up-url.dev" 32 | }, 33 | ]; 34 | 35 | return ( 36 |
37 |
38 | 39 | 40 |
41 | ); 42 | }; 43 | 44 | export default App; 45 | -------------------------------------------------------------------------------- /part9/courseinfo_ts/src/Content.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Part from "./Part"; 3 | import { CoursePart } from "./types"; 4 | 5 | const Content: React.FC<{ parts: CoursePart[] }> = ({ parts }) => ( 6 |
7 | {parts.map((part: CoursePart) => { 8 | return ; 9 | })} 10 |
11 | ); 12 | 13 | export default Content; 14 | -------------------------------------------------------------------------------- /part9/courseinfo_ts/src/Header.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Header: React.FC<{ name: string }> = ({ name }) =>

{name}

; 4 | 5 | export default Header; 6 | -------------------------------------------------------------------------------- /part9/courseinfo_ts/src/Total.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { CoursePart } from "./types"; 3 | 4 | const Total: React.FC<{ parts: CoursePart[] }> = ({ parts }) => ( 5 |
6 | 7 | Total number of exercises:{" "} 8 | {parts.reduce( 9 | (carry: number, part: CoursePart) => carry + part.exerciseCount, 10 | 0 11 | )} 12 | 13 |
14 | ); 15 | 16 | export default Total; 17 | -------------------------------------------------------------------------------- /part9/courseinfo_ts/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./App"; 4 | 5 | ReactDOM.render(, document.getElementById("root")); 6 | -------------------------------------------------------------------------------- /part9/courseinfo_ts/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /part9/courseinfo_ts/src/types.ts: -------------------------------------------------------------------------------- 1 | export type CoursePart = CoursePartOne | CoursePartTwo | CoursePartThree | CoursePartFour; 2 | 3 | interface CoursePartBase { 4 | name: string; 5 | exerciseCount: number; 6 | } 7 | 8 | interface CoursePartWithDescription extends CoursePartBase { 9 | description: string; 10 | } 11 | 12 | export interface CoursePartOne extends CoursePartWithDescription { 13 | name: "Fundamentals"; 14 | } 15 | 16 | export interface CoursePartTwo extends CoursePartBase { 17 | name: "Using props to pass data"; 18 | groupProjectCount: number; 19 | } 20 | 21 | export interface CoursePartThree extends CoursePartWithDescription { 22 | name: "Deeper type usage"; 23 | exerciseSubmissionLink: string; 24 | } 25 | 26 | export interface CoursePartFour extends CoursePartWithDescription { 27 | name: "New course part interface"; 28 | exerciseSubmissionLink: string; 29 | groupProjectCount: number; 30 | } 31 | -------------------------------------------------------------------------------- /part9/courseinfo_ts/src/utils.ts: -------------------------------------------------------------------------------- 1 | // Exhaustive type checking 2 | export const assertNever = (value: never): never => { 3 | throw new Error(`Unhandled discriminated union member: ${JSON.stringify(value)})`); 4 | } 5 | -------------------------------------------------------------------------------- /part9/courseinfo_ts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": false, 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 | -------------------------------------------------------------------------------- /part9/first_steps_with_typescript/.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/first_steps_with_typescript/bmiCalculator.ts: -------------------------------------------------------------------------------- 1 | interface InputValues { 2 | weight: number; 3 | height: number; 4 | } 5 | 6 | interface Query { 7 | weight?: number; 8 | height?: number; 9 | } 10 | 11 | interface Result extends InputValues { 12 | bmi: string; 13 | } 14 | 15 | interface ErrorObject { 16 | error: string; 17 | } 18 | 19 | const parseQuery = (query: Query): InputValues => { 20 | const weight = query.weight; 21 | const height = query.height; 22 | if (!weight || !height || isNaN(weight) || isNaN(height)) { 23 | throw new Error("malformatted parameters"); 24 | } 25 | return { 26 | weight: Number(weight), 27 | height: Number(height) / 100, 28 | }; 29 | }; 30 | 31 | const calculateBmi = (weight: number, height: number): string => { 32 | const result: number = weight / height ** 2; 33 | 34 | if (result < 15) { 35 | return "Very severely underweight"; 36 | } else if (result < 16) { 37 | return "Severely underweight"; 38 | } else if (result < 18.5) { 39 | return "Underweight"; 40 | } else if (result < 25) { 41 | return "Normal (healthy weight)"; 42 | } else if (result < 30) { 43 | return "Overweight"; 44 | } else if (result < 35) { 45 | return "Moderately obese"; 46 | } else if (result < 40) { 47 | return "Severely obese"; 48 | } else { 49 | return "Very severely obese"; 50 | } 51 | }; 52 | 53 | const bmiCalculator = (query: Query): Result | ErrorObject => { 54 | try { 55 | const { weight, height } = parseQuery(query); 56 | const bmi = calculateBmi(weight, height); 57 | return { 58 | weight, 59 | height: height * 100, 60 | bmi, 61 | }; 62 | } catch (err) { 63 | const errorMessage = (err as Error).message; 64 | return { 65 | error: errorMessage, 66 | }; 67 | } 68 | }; 69 | 70 | export default bmiCalculator; 71 | -------------------------------------------------------------------------------- /part9/first_steps_with_typescript/index.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import bmiCalculator from "./bmiCalculator"; 3 | import exerciseCalculator from "./exerciseCalculator"; 4 | 5 | const app = express(); 6 | const PORT = 3003; 7 | 8 | // Middleware 9 | app.use(express.json()); 10 | 11 | // API Endpoints 12 | app.get("/hello", (_req, res) => { 13 | res.send("Hello Full Stack!"); 14 | }); 15 | 16 | app.get("/bmi", (req, res) => { 17 | return res.json(bmiCalculator(req.query)); 18 | }); 19 | 20 | app.post("/exercises", (req, res) => { 21 | return res.json(exerciseCalculator(req.body)); 22 | }); 23 | 24 | // Server 25 | app.listen(PORT, () => { 26 | console.log(`Server ready at http://localhost:${PORT}`); 27 | }); 28 | -------------------------------------------------------------------------------- /part9/first_steps_with_typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "exercise-calculator", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "ts-node": "ts-node", 8 | "calculateBmi": "ts-node bmiCalculator.ts", 9 | "calculateExercises": "ts-node exerciseCalculator.ts", 10 | "dev": "ts-node-dev index.ts", 11 | "lint": "eslint --ext .ts ." 12 | }, 13 | "keywords": [], 14 | "author": "", 15 | "license": "ISC", 16 | "devDependencies": { 17 | "@types/express": "^4.17.7", 18 | "@types/node": "^14.6.0", 19 | "@typescript-eslint/eslint-plugin": "^3.10.1", 20 | "@typescript-eslint/parser": "^3.10.1", 21 | "eslint": "^7.7.0", 22 | "ts-node": "^9.0.0", 23 | "ts-node-dev": "^1.0.0-pre.61", 24 | "typescript": "^4.0.2" 25 | }, 26 | "dependencies": { 27 | "express": "^4.17.1" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /part9/first_steps_with_typescript/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/patientor-backend/.eslintignore: -------------------------------------------------------------------------------- 1 | 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 | { 18 | "argsIgnorePattern": "^_" 19 | } 20 | ], 21 | "@typescript-eslint/no-explicit-any": 1, 22 | "no-case-declarations": 0 23 | }, 24 | "parser": "@typescript-eslint/parser", 25 | "parserOptions": { 26 | "project": "./tsconfig.json" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /part9/patientor-backend/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build/ 3 | -------------------------------------------------------------------------------- /part9/patientor-backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "patientor-backend", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "tsc": "tsc", 8 | "dev": "ts-node-dev src/index.ts", 9 | "lint": "eslint --ext .ts .", 10 | "start": "node build/src/index.js" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "ISC", 15 | "devDependencies": { 16 | "@types/cors": "^2.8.7", 17 | "@types/express": "^4.17.7", 18 | "@types/uuid": "^8.3.0", 19 | "@typescript-eslint/eslint-plugin": "^3.10.1", 20 | "@typescript-eslint/parser": "^3.10.1", 21 | "eslint": "^7.11.0", 22 | "ts-node-dev": "^1.0.0-pre.61", 23 | "typescript": "^4.0.2" 24 | }, 25 | "dependencies": { 26 | "cors": "^2.8.5", 27 | "express": "^4.17.1", 28 | "uuid": "^8.3.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /part9/patientor-backend/src/index.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import cors from "cors"; 3 | import diagnosesRouter from "./routes/diagnosesRouter"; 4 | import patientsRouter from "./routes/patientsRouter"; 5 | 6 | const app = express(); 7 | 8 | // Config 9 | const PORT = 3001; 10 | 11 | // Middleware 12 | app.use(express.json()); 13 | app.use(cors()); 14 | 15 | // Routes 16 | app.get("/api/ping", (_req, res) => res.send("pong")); 17 | app.use("/api/diagnoses", diagnosesRouter); 18 | app.use("/api/patients", patientsRouter); 19 | 20 | // Server 21 | app.listen(PORT, () => { 22 | console.log(`Server ready at http://localhost:${PORT}`); 23 | }); 24 | -------------------------------------------------------------------------------- /part9/patientor-backend/src/routes/diagnosesRouter.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import diagnosesService from "../services/diagnosesService"; 3 | 4 | const router = express.Router(); 5 | 6 | router.get("/", (_req, res) => { 7 | res.send(diagnosesService.getAllDiagnoses()); 8 | }); 9 | 10 | export default router; 11 | -------------------------------------------------------------------------------- /part9/patientor-backend/src/routes/patientsRouter.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import patientsService from "../services/patientsService"; 3 | import { toNewPatient, toNewEntry } from "../utils"; 4 | 5 | const router = express.Router(); 6 | 7 | router.get("/", (_req, res) => { 8 | res.send(patientsService.getCensoredPatients()); 9 | }); 10 | 11 | router.get("/:id", (req, res) => { 12 | try { 13 | const patient = patientsService.getPatient(req.params.id); 14 | res.json(patient); 15 | } catch (error) { 16 | const message = (error as Error).message; 17 | res.status(404).send({ error: message }); 18 | } 19 | }); 20 | 21 | router.post("/:id/entries", (req, res) => { 22 | try { 23 | const patient = patientsService.getPatient(req.params.id); 24 | if (!patient) { 25 | throw new Error("Patient not found"); 26 | } 27 | const newEntry = toNewEntry(req.body); 28 | const updatedPatient = patientsService.addEntry(patient, newEntry); 29 | res.json(updatedPatient); 30 | } catch (error) { 31 | const message = (error as Error).message; 32 | res.status(400).send({ error: message }); 33 | } 34 | }); 35 | 36 | router.post("/", (req, res) => { 37 | try { 38 | const newPatientData = toNewPatient(req.body); 39 | const newPatient = patientsService.addPatient(newPatientData); 40 | res.json(newPatient); 41 | } catch (error) { 42 | const message = (error as Error).message; 43 | res.status(400).send({ error: message }); 44 | } 45 | }); 46 | 47 | export default router; 48 | -------------------------------------------------------------------------------- /part9/patientor-backend/src/services/diagnosesService.ts: -------------------------------------------------------------------------------- 1 | import { Diagnosis } from "../types"; 2 | import diagnosesData from "../../data/diagnoses"; 3 | 4 | const diagnoses: Diagnosis[] = diagnosesData; 5 | 6 | const getAllDiagnoses = (): Diagnosis[] => { 7 | return diagnoses; 8 | }; 9 | 10 | export default { 11 | getAllDiagnoses 12 | }; 13 | -------------------------------------------------------------------------------- /part9/patientor-backend/src/services/patientsService.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuid } from "uuid"; 2 | import patientsData from "../../data/patients"; 3 | import { NewPatient, Patient, CensoredPatient, NewEntry } from "../types"; 4 | import { censorPatient } from "../utils"; 5 | 6 | let patients: Patient[] = patientsData; 7 | 8 | const getCensoredPatients = (): CensoredPatient[] => { 9 | return patients.map((patient) => censorPatient(patient)); 10 | }; 11 | 12 | const addPatient = (patient: NewPatient): CensoredPatient => { 13 | const newPatient = { 14 | id: uuid(), 15 | ...patient, 16 | }; 17 | patients.push(newPatient); 18 | return censorPatient(newPatient); 19 | }; 20 | 21 | const getPatients = (): Patient[] => { 22 | return patients; 23 | }; 24 | 25 | const getPatient = (id: string): Patient => { 26 | const patient = patients.find((p) => p.id === id); 27 | 28 | if (!patient) { 29 | throw new Error("Patient not found"); 30 | } 31 | 32 | return patient; 33 | }; 34 | 35 | const addEntry = (patient: Patient, newEntry: NewEntry): Patient => { 36 | const entry = { ...newEntry, id: uuid() }; 37 | const updatedPatient = { 38 | ...patient, 39 | entries: patient.entries?.concat(entry), 40 | }; 41 | patients = patients.map((p) => { 42 | if (p.id === updatedPatient.id) { 43 | return updatedPatient; 44 | } 45 | return p; 46 | }); 47 | 48 | return updatedPatient; 49 | }; 50 | 51 | export default { 52 | getPatients, 53 | getCensoredPatients, 54 | getPatient, 55 | addPatient, 56 | addEntry, 57 | }; 58 | -------------------------------------------------------------------------------- /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 | } 13 | } 14 | -------------------------------------------------------------------------------- /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/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /part9/patientor-frontend/README.md: -------------------------------------------------------------------------------- 1 | # Patientor - frontend 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm install` 10 | 11 | Install the project dependencies. 12 | 13 | ### `npm start` 14 | 15 | Runs the app in the development mode.
16 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 17 | 18 | The page will reload if you make edits.
19 | You will also see any lint errors in the console. 20 | 21 | ### `npm test` 22 | 23 | Launches the test runner in the interactive watch mode.
24 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 25 | 26 | ### `npm build` 27 | 28 | Builds the app for production to the `build` folder.
29 | It correctly bundles React in production mode and optimizes the build for the best performance. 30 | 31 | The build is minified and the filenames include the hashes.
32 | Your app is ready to be deployed! 33 | 34 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 35 | 36 | ## Learn More 37 | 38 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 39 | 40 | To learn React, check out the [React documentation](https://reactjs.org/). 41 | -------------------------------------------------------------------------------- /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.11.0", 9 | "react-dom": "^16.11.0", 10 | "react-router-dom": "^5.1.2", 11 | "semantic-ui-css": "^2.4.1", 12 | "semantic-ui-react": "^0.88.1" 13 | }, 14 | "devDependencies": { 15 | "@types/axios": "^0.14.0", 16 | "@types/jest": "24.0.19", 17 | "@types/node": "12.11.7", 18 | "@types/react": "^16.9.11", 19 | "@types/react-dom": "16.9.3", 20 | "@types/react-router-dom": "^5.1.2", 21 | "@typescript-eslint/eslint-plugin": "^2.12.0", 22 | "@typescript-eslint/parser": "^2.12.0", 23 | "eslint-config-react": "^1.1.7", 24 | "react-scripts": "3.3.0", 25 | "typescript": "^3.7.0" 26 | }, 27 | "scripts": { 28 | "start": "react-scripts start", 29 | "build": "react-scripts build", 30 | "test": "react-scripts test", 31 | "eject": "react-scripts eject", 32 | "lint": "eslint './src/**/*.{ts,tsx}'", 33 | "lint:fix": "eslint './src/**/*.{ts,tsx}' --fix" 34 | }, 35 | "browserslist": { 36 | "production": [ 37 | ">0.2%", 38 | "not dead", 39 | "not op_mini all" 40 | ], 41 | "development": [ 42 | "last 1 chrome version", 43 | "last 1 firefox version", 44 | "last 1 safari version" 45 | ] 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /part9/patientor-frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orrsteinberg/fullstackopen-2020/2565b48898ac7249e645ba3ba96fdef4780b782a/part9/patientor-frontend/public/favicon.ico -------------------------------------------------------------------------------- /part9/patientor-frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 16 | 17 | 26 | Diagnosis app 27 | 28 | 29 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /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 | import AddEntryForm from "./AddEntryForm"; 5 | 6 | interface Props { 7 | modalOpen: boolean; 8 | onClose: () => void; 9 | onSubmit: (newEntry: NewEntry) => void; 10 | error?: string | null; 11 | } 12 | 13 | const AddEntryModal = ({ modalOpen, onClose, onSubmit, error }: Props) => ( 14 | 15 | Add a new entry 16 | 17 | {error && {`Error: ${error}`}} 18 | 19 | 20 | 21 | ); 22 | 23 | export default AddEntryModal; 24 | -------------------------------------------------------------------------------- /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: ${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 { List } from "semantic-ui-react"; 3 | import { useStateValue } from "../state"; 4 | import { Diagnosis } from "../types"; 5 | 6 | const DiagnosisList: React.FC<{ 7 | diagnosisCodes: Array; 8 | }> = ({ diagnosisCodes }) => { 9 | const [{ diagnoses }] = useStateValue(); 10 | 11 | return ( 12 | 13 | {diagnosisCodes.map((code) => ( 14 | 19 | ))} 20 | 21 | ); 22 | }; 23 | 24 | export default DiagnosisList; 25 | -------------------------------------------------------------------------------- /part9/patientor-frontend/src/PatientPage/EntryDetails.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Entry, EntryType } from "../types"; 3 | import { assertNever } from "../utils"; 4 | import { 5 | HospitalEntry, 6 | OccupationalHealthcareEntry, 7 | HealthCheckEntry, 8 | } from "./Entries"; 9 | 10 | const EntryDetails: React.FC<{ entry: Entry }> = ({ entry }) => { 11 | switch (entry.type) { 12 | case EntryType.Hospital: 13 | return ; 14 | case EntryType.OccupationalHealthcare: 15 | return ; 16 | case EntryType.HealthCheck: 17 | return ; 18 | default: 19 | return assertNever(entry); 20 | } 21 | }; 22 | 23 | export default EntryDetails; 24 | -------------------------------------------------------------------------------- /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/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/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { NewEntry, NewBaseEntry, EntryFormValues, EntryType } from "./types"; 2 | 3 | export const assertNever = (value: never): never => { 4 | throw new Error( 5 | `Unhandled discriminated union member: ${JSON.stringify(value)}` 6 | ); 7 | }; 8 | 9 | const isDate = (date: any): boolean => { 10 | return Boolean(Date.parse(date)); 11 | }; 12 | 13 | export const isValidDate = (date: any): boolean => { 14 | const regEx = /^\d{4}-\d{2}-\d{2}$/; 15 | return isDate(date) && date.match(regEx); 16 | }; 17 | 18 | export const toNewEntry = (entryFormValues: EntryFormValues): NewEntry => { 19 | const { 20 | type, 21 | description, 22 | date, 23 | specialist, 24 | diagnosisCodes, 25 | } = entryFormValues; 26 | const newBaseEntry: NewBaseEntry = { 27 | description, 28 | date, 29 | specialist, 30 | diagnosisCodes, 31 | }; 32 | 33 | switch (type) { 34 | case EntryType.HealthCheck: 35 | return { 36 | ...newBaseEntry, 37 | type, 38 | healthCheckRating: entryFormValues.healthCheckRating, 39 | }; 40 | 41 | case EntryType.OccupationalHealthcare: 42 | return { 43 | ...newBaseEntry, 44 | type, 45 | employerName: entryFormValues.employerName, 46 | sickLeave: { 47 | startDate: entryFormValues.sickLeaveStartDate, 48 | endDate: entryFormValues.sickLeaveEndDate, 49 | }, 50 | }; 51 | 52 | case EntryType.Hospital: 53 | return { 54 | ...newBaseEntry, 55 | type, 56 | discharge: { 57 | date: entryFormValues.dischargeDate, 58 | criteria: entryFormValues.dischargeCriteria, 59 | }, 60 | }; 61 | 62 | default: 63 | return assertNever(type); 64 | } 65 | }; 66 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------