├── .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 |
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 |
56 |
57 |
58 |
59 | >
60 | );
61 | };
62 |
63 | ReactDOM.render(, document.getElementById("root"));
64 |
--------------------------------------------------------------------------------
/part2/countries/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "countries",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^4.2.4",
7 | "@testing-library/react": "^9.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 | },
20 | "eslintConfig": {
21 | "extends": "react-app"
22 | },
23 | "browserslist": {
24 | "production": [
25 | ">0.2%",
26 | "not dead",
27 | "not op_mini all"
28 | ],
29 | "development": [
30 | "last 1 chrome version",
31 | "last 1 firefox version",
32 | "last 1 safari version"
33 | ]
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/part2/countries/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/orrsteinberg/fullstackopen-2020/2565b48898ac7249e645ba3ba96fdef4780b782a/part2/countries/public/favicon.ico
--------------------------------------------------------------------------------
/part2/countries/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/countries/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/orrsteinberg/fullstackopen-2020/2565b48898ac7249e645ba3ba96fdef4780b782a/part2/countries/public/logo192.png
--------------------------------------------------------------------------------
/part2/countries/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/orrsteinberg/fullstackopen-2020/2565b48898ac7249e645ba3ba96fdef4780b782a/part2/countries/public/logo512.png
--------------------------------------------------------------------------------
/part2/countries/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/part2/countries/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/part2/countries/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import axios from "axios";
3 | import Country from "./components/Country";
4 | import CountryInfo from "./components/CountryInfo";
5 |
6 | const App = () => {
7 | const [countries, setCountries] = useState([]);
8 | const [search, setSearch] = useState("");
9 |
10 | useEffect(() => {
11 | axios
12 | .get("https://restcountries.eu/rest/v2/all")
13 | .then((response) => setCountries(response.data))
14 | .catch((error) => console.error(error));
15 | }, []);
16 |
17 | const handleSearchChange = (event) => setSearch(event.target.value);
18 |
19 | const countriesToShow =
20 | search === ""
21 | ? []
22 | : countries.filter((country) =>
23 | country.name.toLowerCase().includes(search.toLowerCase())
24 | );
25 |
26 | if (countriesToShow.length === 1) {
27 | return (
28 |
29 | Find countries
30 |
31 |
32 |
33 |
34 | );
35 | }
36 |
37 | return (
38 |
39 | Find countries
40 |
41 | {countriesToShow.length > 10
42 | ? "Too many matches, specify another filter"
43 | : countriesToShow.map((country) => (
44 |
45 |
46 |
47 | ))}
48 |
49 |
50 | );
51 | };
52 |
53 | export default App;
54 |
--------------------------------------------------------------------------------
/part2/countries/src/components/Country.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import CountryInfo from "./CountryInfo";
3 |
4 | const Country = ({ country }) => {
5 | const [show, setShow] = useState(false);
6 |
7 | const handleButtonClick = () => setShow(!show);
8 |
9 | if (show) {
10 | return (
11 |
12 | {country.name}{" "}
13 |
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 |

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 |
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 |
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 |
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 |
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 |
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 |
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 | Born |
34 | Books |
35 |
36 | {authors.map((a) => (
37 |
38 | {a.name} |
39 | {a.born} |
40 | {a.bookCount} |
41 |
42 | ))}
43 |
44 |
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 |
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 | Author |
43 | Published |
44 |
45 | {recommendedBooks.map((a) => (
46 |
47 | {a.title} |
48 | {a.author.name} |
49 | {a.published} |
50 |
51 | ))}
52 |
53 |
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 |
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 |
--------------------------------------------------------------------------------