├── .gitignore
├── README.md
├── part0
├── 0.4_new_note.png
├── 0.5_spa.png
├── 0.6_new_note_spa.png
└── README.md
├── part1
├── README.md
├── anecdotes
│ ├── .gitignore
│ ├── README.md
│ ├── package.json
│ ├── public
│ │ ├── favicon.ico
│ │ ├── index.html
│ │ ├── logo192.png
│ │ ├── logo512.png
│ │ ├── manifest.json
│ │ └── robots.txt
│ ├── src
│ │ ├── index.css
│ │ ├── index.js
│ │ └── setupTests.js
│ └── yarn.lock
├── courseinfo
│ ├── .gitignore
│ ├── README.md
│ ├── package.json
│ ├── public
│ │ ├── favicon.ico
│ │ ├── index.html
│ │ ├── logo192.png
│ │ ├── logo512.png
│ │ ├── manifest.json
│ │ └── robots.txt
│ ├── src
│ │ ├── index.css
│ │ ├── index.js
│ │ └── setupTests.js
│ └── yarn.lock
└── unicafe
│ ├── .gitignore
│ ├── README.md
│ ├── package.json
│ ├── public
│ ├── favicon.ico
│ ├── index.html
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── robots.txt
│ ├── src
│ ├── index.css
│ ├── index.js
│ └── setupTests.js
│ └── yarn.lock
├── part2
├── README.md
├── countries
│ ├── .gitignore
│ ├── README.md
│ ├── package.json
│ ├── public
│ │ ├── favicon.ico
│ │ ├── index.html
│ │ ├── logo192.png
│ │ ├── logo512.png
│ │ ├── manifest.json
│ │ └── robots.txt
│ ├── src
│ │ ├── App.js
│ │ ├── components
│ │ │ ├── Content.js
│ │ │ ├── Country.js
│ │ │ └── Filter.js
│ │ ├── index.css
│ │ ├── index.js
│ │ └── setupTests.js
│ └── yarn.lock
├── coursecontents
│ ├── .gitignore
│ ├── README.md
│ ├── package.json
│ ├── public
│ │ ├── favicon.ico
│ │ ├── index.html
│ │ ├── logo192.png
│ │ ├── logo512.png
│ │ ├── manifest.json
│ │ └── robots.txt
│ ├── src
│ │ ├── App.js
│ │ ├── components
│ │ │ ├── Content.js
│ │ │ ├── Course.js
│ │ │ ├── Header.js
│ │ │ ├── Part.js
│ │ │ └── Total.js
│ │ ├── index.css
│ │ ├── index.js
│ │ └── setupTests.js
│ └── yarn.lock
└── phonebook
│ ├── .gitignore
│ ├── README.md
│ ├── db.json
│ ├── package.json
│ ├── public
│ ├── favicon.ico
│ ├── index.html
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── robots.txt
│ ├── src
│ ├── App.js
│ ├── components
│ │ ├── Content.js
│ │ ├── Filter.js
│ │ ├── Notification.js
│ │ ├── Person.js
│ │ └── PersonForm.js
│ ├── index.css
│ ├── index.js
│ ├── services
│ │ └── persons.js
│ └── setupTests.js
│ └── yarn.lock
├── part3
├── README.md
└── phonebook
│ ├── .eslintignore
│ ├── .eslintrc.json
│ ├── .gitignore
│ ├── Procfile
│ ├── README.md
│ ├── build
│ ├── asset-manifest.json
│ ├── favicon.ico
│ ├── index.html
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ ├── precache-manifest.7421c121b71b8f1dc271af0189e73b64.js
│ ├── robots.txt
│ ├── service-worker.js
│ └── static
│ │ └── js
│ │ ├── 2.d50c3fcf.chunk.js
│ │ ├── 2.d50c3fcf.chunk.js.LICENSE.txt
│ │ ├── 2.d50c3fcf.chunk.js.map
│ │ ├── main.2bb538bb.chunk.js
│ │ ├── main.2bb538bb.chunk.js.map
│ │ ├── runtime-main.4464e187.js
│ │ └── runtime-main.4464e187.js.map
│ ├── index.js
│ ├── models
│ └── person.js
│ ├── package-lock.json
│ └── package.json
├── part4
├── README.md
└── bloglist
│ ├── .eslintrc.json
│ ├── .gitignore
│ ├── README.md
│ ├── app.js
│ ├── controllers
│ ├── blogs.js
│ ├── login.js
│ ├── testing.js
│ └── users.js
│ ├── index.js
│ ├── jest.config.js
│ ├── jest.setup.js
│ ├── models
│ ├── blog.js
│ └── user.js
│ ├── package-lock.json
│ ├── package.json
│ ├── tests
│ ├── bloglist_api.test.js
│ ├── helper.test.js
│ ├── test_helper.js
│ └── user_api.test.js
│ └── utils
│ ├── config.js
│ ├── list_helper.js
│ ├── logger.js
│ └── middleware.js
├── part5
├── README.md
└── bloglist-frontend
│ ├── .editorconfig
│ ├── .eslintignore
│ ├── .eslintrc.js
│ ├── .gitignore
│ ├── README.md
│ ├── cypress.json
│ ├── cypress
│ ├── fixtures
│ │ └── example.json
│ ├── integration
│ │ └── blog_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
│ ├── BlogForm.js
│ ├── BlogForm.test.js
│ ├── LoginForm.js
│ ├── Notification.js
│ └── Togglable.js
│ ├── index.js
│ ├── services
│ ├── blogs.js
│ └── login.js
│ └── setupTests.js
├── part6
├── README.md
├── redux-anecdotes
│ ├── .gitignore
│ ├── 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
│ │ ├── AnecdoteForm.js
│ │ ├── AnecdoteList.js
│ │ ├── Filter.js
│ │ └── Notification.js
│ │ ├── index.js
│ │ ├── reducers
│ │ ├── anecdoteReducer.js
│ │ ├── filterReducer.js
│ │ └── notificationReducer.js
│ │ ├── services
│ │ └── anecdotes.js
│ │ └── utils
│ │ └── store.js
└── unicafe-redux
│ ├── .gitignore
│ ├── README.md
│ ├── package-lock.json
│ ├── package.json
│ ├── public
│ ├── favicon.ico
│ ├── index.html
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── robots.txt
│ └── src
│ ├── index.js
│ └── reducers
│ ├── counterReducer.js
│ └── counterReducer.test.js
├── part7
├── README.md
├── bloglist
│ ├── .editorconfig
│ ├── .eslintignore
│ ├── .eslintrc.js
│ ├── .gitignore
│ ├── .prettierrc.js
│ ├── README.md
│ ├── cypress.json
│ ├── cypress
│ │ ├── fixtures
│ │ │ └── example.json
│ │ ├── integration
│ │ │ └── blog_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
│ │ ├── BlogForm.js
│ │ ├── BlogForm.test.js
│ │ ├── BlogList.js
│ │ ├── Header.js
│ │ ├── LoginForm.js
│ │ ├── Notification.js
│ │ ├── Togglable.js
│ │ └── UserList.js
│ │ ├── index.js
│ │ ├── reducers
│ │ ├── authReducer.js
│ │ ├── blogReducer.js
│ │ ├── notificationReducer.js
│ │ └── userReducer.js
│ │ ├── services
│ │ ├── blogs.js
│ │ ├── login.js
│ │ └── users.js
│ │ ├── setupTests.js
│ │ └── utils
│ │ └── store.js
├── country-hook
│ ├── .gitignore
│ ├── 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
│ │ ├── hooks
│ │ └── index.js
│ │ └── index.js
├── routed-anecdotes
│ ├── .eslintignore
│ ├── .eslintrc.js
│ ├── .gitignore
│ ├── .prettierrc.js
│ ├── README.md
│ ├── package-lock.json
│ ├── package.json
│ ├── public
│ │ ├── favicon.ico
│ │ ├── index.html
│ │ ├── logo192.png
│ │ ├── logo512.png
│ │ ├── manifest.json
│ │ └── robots.txt
│ └── src
│ │ ├── App.js
│ │ ├── components
│ │ ├── About.js
│ │ ├── Anecdote.js
│ │ ├── AnecdoteList.js
│ │ ├── CreateNew.js
│ │ ├── Footer.js
│ │ ├── Menu.js
│ │ └── Notification.js
│ │ ├── hooks
│ │ └── index.js
│ │ └── index.js
└── ultimate-hooks
│ ├── .gitignore
│ ├── 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
│ ├── hooks
│ └── index.js
│ └── index.js
├── part8
├── README.md
├── library-backend
│ ├── README.md
│ ├── index.js
│ ├── models
│ │ ├── author.js
│ │ ├── book.js
│ │ └── users.js
│ ├── package-lock.json
│ └── package.json
└── library-frontend
│ ├── .gitignore
│ ├── README.md
│ ├── package-lock.json
│ ├── package.json
│ ├── public
│ ├── favicon.ico
│ ├── index.html
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── robots.txt
│ └── src
│ ├── App.js
│ ├── components
│ ├── Authors.js
│ ├── BookForm.js
│ ├── Books.js
│ ├── BornYearForm.js
│ ├── LoginForm.js
│ └── Recommended.js
│ ├── index.js
│ └── queries.js
└── part9
├── README.md
├── courseinfo
├── .eslintrc
├── .gitignore
├── README.md
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── robots.txt
├── src
│ ├── App.tsx
│ ├── components
│ │ ├── Content.tsx
│ │ ├── Header.tsx
│ │ ├── Part.tsx
│ │ └── Total.tsx
│ ├── index.tsx
│ ├── react-app-env.d.ts
│ └── types.ts
├── tsconfig.json
└── yarn.lock
├── first-steps
├── .eslintrc
├── .gitignore
├── .prettierrc
├── README.md
├── bmiCalculator.ts
├── exerciseCalculator.ts
├── index.ts
├── package-lock.json
├── package.json
└── tsconfig.json
├── patientor-backend
├── .eslintignore
├── .eslintrc
├── .prettierrc
├── README.md
├── data
│ ├── diagnoses.ts
│ └── patients.ts
├── package-lock.json
├── package.json
├── src
│ ├── index.ts
│ ├── routes
│ │ ├── diagnoses.ts
│ │ └── patients.ts
│ ├── services
│ │ ├── diagnoseService.ts
│ │ └── patientService.ts
│ ├── types.ts
│ └── utils.ts
└── tsconfig.json
└── patientor-frontend
├── .eslintrc
├── .gitignore
├── .prettierrc
├── README.md
├── package-lock.json
├── package.json
├── public
├── favicon.ico
├── index.html
└── manifest.json
├── src
├── AddPatientModal
│ ├── AddPatientForm.tsx
│ ├── FormField.tsx
│ └── index.tsx
├── App.tsx
├── PatientListPage
│ └── index.tsx
├── PatientPage
│ ├── HealthCheck.tsx
│ ├── Hospital.tsx
│ ├── OccupationalHealthcare.tsx
│ └── index.tsx
├── components
│ └── HealthRatingBar.tsx
├── constants.ts
├── index.tsx
├── react-app-env.d.ts
├── state
│ ├── index.ts
│ ├── reducer.ts
│ └── state.tsx
└── types.ts
└── tsconfig.json
/.gitignore:
--------------------------------------------------------------------------------
1 | # dependencies
2 | .node_modules
3 | /.pnp
4 | .pnp.js
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 | .env
19 |
20 | npm-debug.log*
21 | yarn-debug.log*
22 | yarn-error.log*
23 |
24 | **/**/dist/
25 | **/**/node_modules/
26 | .idea
27 |
28 | .firebase
29 |
30 | .runtimeconfig.json
31 |
32 | # compiled output
33 | /dist
34 |
35 | # Logs
36 | logs
37 | *.log
38 | lerna-debug.log*
39 |
40 | # Tests
41 | /.nyc_output
42 |
43 | # IDEs and editors
44 | .idea/
45 | .project
46 | .classpath
47 | .c9/
48 | *.launch
49 | .settings/
50 | *.sublime-workspace
51 |
52 | # IDE - VSCode
53 | .vscode/
54 |
55 | storybook-static
56 | *.sqlite
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # [Full Stack Open 2020](https://fullstackopen.com/en/)
2 |
3 | This course is held at the Department of Computer Science at the University of Helsinki in Spring 2020.
4 |
5 | It serves as an introduction to modern web application development with JavaScript. The main focus is on building single page applications with ReactJS that use REST APIs built with Node.js.
6 |
7 | GraphQL, a modern alternative to REST APIs is also covered by this course. As well as testing, configuration & environment management, and the use of MongoDB for storing the application’s data. A part on TypeScript can also be found on this year's edition. This repository contains my solutions for this course's exercises.
8 |
9 | [couse certificate](https://studies.cs.helsinki.fi/stats/api/certificate/fullstackopen/en/fff9bc0633b27820d3a04756dd40455e)
10 |
11 | ### [Part 0 - Fundamentals of Web apps](./part0)
12 |
13 | ### [Part 1 - Introduction to React](./part1)
14 |
15 | ### [Part 2 - Communicating with server](./part2)
16 |
17 | ### [Part 3 - Programming a server with NodeJS and Express](./part3)
18 |
19 | ### [Part 4 - Testing Express servers, user administration](./part4)
20 |
21 | ### [Part 5 - Testing React apps](./part5)
22 |
23 | ### [Part 6 - State management with Redux](./part6)
24 |
25 | ### [Part 7 - React router, custom hooks, styling app with CSS and webpack](./part7)
26 |
27 | ### [Part 8 - GraphQL](./part8)
28 |
29 | ### [Part 9 - Typescript](./part9)
--------------------------------------------------------------------------------
/part0/0.4_new_note.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anancarv/fullstackopen/3b12bbb3f9ca4ca8c5b9c29df558d787e1009e8c/part0/0.4_new_note.png
--------------------------------------------------------------------------------
/part0/0.5_spa.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anancarv/fullstackopen/3b12bbb3f9ca4ca8c5b9c29df558d787e1009e8c/part0/0.5_spa.png
--------------------------------------------------------------------------------
/part0/0.6_new_note_spa.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anancarv/fullstackopen/3b12bbb3f9ca4ca8c5b9c29df558d787e1009e8c/part0/0.6_new_note_spa.png
--------------------------------------------------------------------------------
/part0/README.md:
--------------------------------------------------------------------------------
1 | # Part 0
2 |
3 | In this part, we will familiarize ourselves with the practicalities of taking the course. After that we will have an overview of the basics of web development, and also talk about the advances in web application development during the last few decades.
--------------------------------------------------------------------------------
/part1/README.md:
--------------------------------------------------------------------------------
1 | # Solutions of part 1 exercises
2 |
3 | In this part, we will familiarize ourselves with the React-library, which we will be using to write the code that runs in the browser. We will also look at some features of Javascript that are important for understanding React.
4 |
5 | ## Requirements
6 | * [node](https://nodejs.org/en/download/)
7 | * [yarn](https://classic.yarnpkg.com/en/docs/install/#debian-stable)
8 |
9 |
10 | ## Start the application
11 |
12 | There is one application by folder, to start an application :
13 |
14 | ```bash
15 | # Head to the desired exercise (courseinfo, unicafe or anecdotes)
16 | $ cd courseinfo
17 | # Start the application
18 | $ yarn start
19 | ```
20 |
21 | You can then access the frontend on : [http://localhost:3000/](http://localhost:3000/)
--------------------------------------------------------------------------------
/part1/anecdotes/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/part1/anecdotes/README.md:
--------------------------------------------------------------------------------
1 | # Anecdotes
2 |
3 | The world of software engineering is filled with anecdotes that distill timeless truths from our field into short one-liners.
4 |
5 | This application allows the user to vote between multiple anecdotes and then displays the most popular one.
6 |
7 | ## Start the application
8 |
9 | To start an application, do the following :
10 |
11 | ```bash
12 | # Install dependancies
13 | $ yarn install
14 | # Start the application
15 | $ yarn start
16 | ```
17 |
18 | You can then access the app on : [http://localhost:3000/](http://localhost:3000/)
--------------------------------------------------------------------------------
/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.3.2",
8 | "@testing-library/user-event": "^7.1.2",
9 | "react": "^16.13.1",
10 | "react-dom": "^16.13.1",
11 | "react-scripts": "3.4.1"
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/anancarv/fullstackopen/3b12bbb3f9ca4ca8c5b9c29df558d787e1009e8c/part1/anecdotes/public/favicon.ico
--------------------------------------------------------------------------------
/part1/anecdotes/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anancarv/fullstackopen/3b12bbb3f9ca4ca8c5b9c29df558d787e1009e8c/part1/anecdotes/public/logo192.png
--------------------------------------------------------------------------------
/part1/anecdotes/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anancarv/fullstackopen/3b12bbb3f9ca4ca8c5b9c29df558d787e1009e8c/part1/anecdotes/public/logo512.png
--------------------------------------------------------------------------------
/part1/anecdotes/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/part1/anecdotes/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/part1/anecdotes/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/part1/anecdotes/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom/extend-expect';
6 |
--------------------------------------------------------------------------------
/part1/courseinfo/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/part1/courseinfo/README.md:
--------------------------------------------------------------------------------
1 | # Course information
2 |
3 | Simple web applicaton for understanding the core concepts of React
4 |
5 | ## Start the application
6 |
7 | To start an application, do the following :
8 |
9 | ```bash
10 | # Install dependancies
11 | $ yarn install
12 | # Start the application
13 | $ yarn start
14 | ```
15 |
16 | You can then access the app on : [http://localhost:3000/](http://localhost:3000/)
--------------------------------------------------------------------------------
/part1/courseinfo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "courseinfo",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^4.2.4",
7 | "@testing-library/react": "^9.3.2",
8 | "@testing-library/user-event": "^7.1.2",
9 | "react": "^16.13.1",
10 | "react-dom": "^16.13.1",
11 | "react-scripts": "3.4.1"
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/anancarv/fullstackopen/3b12bbb3f9ca4ca8c5b9c29df558d787e1009e8c/part1/courseinfo/public/favicon.ico
--------------------------------------------------------------------------------
/part1/courseinfo/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anancarv/fullstackopen/3b12bbb3f9ca4ca8c5b9c29df558d787e1009e8c/part1/courseinfo/public/logo192.png
--------------------------------------------------------------------------------
/part1/courseinfo/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anancarv/fullstackopen/3b12bbb3f9ca4ca8c5b9c29df558d787e1009e8c/part1/courseinfo/public/logo512.png
--------------------------------------------------------------------------------
/part1/courseinfo/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/part1/courseinfo/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/part1/courseinfo/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/part1/courseinfo/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 |
4 | const Header = (props) => {
5 | return (
6 |
{props.course}
7 | )
8 | }
9 |
10 | const Part = (props) => {
11 | return (
12 |
13 | {props.part} {props.exercises}
14 |
15 | )
16 | }
17 |
18 | const Content = (props) => {
19 | return (
20 |
25 | )
26 | }
27 |
28 | const Total = (props) => {
29 | return (
30 | Number of exercises {props.parts[0].exercises + props.parts[1].exercises + props.parts[2].exercises}
31 | )
32 | }
33 |
34 | const App = () => {
35 | const course = {
36 | name: 'Half Stack application development',
37 | parts: [
38 | {
39 | name: 'Fundamentals of React',
40 | exercises: 10
41 | },
42 | {
43 | name: 'Using props to pass data',
44 | exercises: 7
45 | },
46 | {
47 | name: 'State of a component',
48 | exercises: 14
49 | }
50 | ]
51 | }
52 |
53 | return (
54 |
55 |
56 |
57 |
58 |
59 | )
60 | }
61 |
62 | ReactDOM.render( , document.getElementById('root'))
--------------------------------------------------------------------------------
/part1/courseinfo/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom/extend-expect';
6 |
--------------------------------------------------------------------------------
/part1/unicafe/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/part1/unicafe/README.md:
--------------------------------------------------------------------------------
1 | # Unicafe
2 |
3 | This web application collects customer feedback. There are only three options for feedback: good, neutral, and bad.
4 |
5 | While collecting feedbacks, the following statistics are displayed:
6 | * Total number of colllected feedbacks
7 | * The average score (good: 1, neutral: 0, bad: -1)
8 | * The percentage of positive feedback
9 |
10 | ## Start the application
11 |
12 | To start an application, do the following :
13 |
14 | ```bash
15 | # Install dependancies
16 | $ yarn install
17 | # Start the application
18 | $ yarn start
19 | ```
20 |
21 | You can then access the app on : [http://localhost:3000/](http://localhost:3000/)
--------------------------------------------------------------------------------
/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.3.2",
8 | "@testing-library/user-event": "^7.1.2",
9 | "react": "^16.13.1",
10 | "react-dom": "^16.13.1",
11 | "react-scripts": "3.4.1"
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/anancarv/fullstackopen/3b12bbb3f9ca4ca8c5b9c29df558d787e1009e8c/part1/unicafe/public/favicon.ico
--------------------------------------------------------------------------------
/part1/unicafe/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anancarv/fullstackopen/3b12bbb3f9ca4ca8c5b9c29df558d787e1009e8c/part1/unicafe/public/logo192.png
--------------------------------------------------------------------------------
/part1/unicafe/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anancarv/fullstackopen/3b12bbb3f9ca4ca8c5b9c29df558d787e1009e8c/part1/unicafe/public/logo512.png
--------------------------------------------------------------------------------
/part1/unicafe/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/part1/unicafe/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/part1/unicafe/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/part1/unicafe/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom/extend-expect';
6 |
--------------------------------------------------------------------------------
/part2/README.md:
--------------------------------------------------------------------------------
1 | # Solutions of part 2 exercises
2 |
3 | In this part, we will first take a look at how to render a data collection, like a list of names, to the screen. After this, we will inspect how a user can submit data to a React application using HTML forms. Next, our focus shifts towards looking at how JavaScript code in the browser can fetch and handle data stored in a remote backend server. Lastly, we will take a quick look at a few simple ways of adding CSS styles to our React applications.
--------------------------------------------------------------------------------
/part2/countries/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/part2/countries/README.md:
--------------------------------------------------------------------------------
1 | # Countries
2 |
3 | In this exercise, we created an application, in which one can look at data of various countries. The data are fetched from the API https://restcountries.eu, that provides a lot data for different countries in a machine readable format, a so-called REST API.
4 |
5 | The user interface is very simple. The country to be shown is found by typing a search query into the search field.
6 |
7 | In this application, it is also possible to see the current weather in the country's capital.
8 |
9 | ## Start the application
10 |
11 | To start an application, do the following :
12 |
13 | ```bash
14 | # Install dependancies
15 | $ yarn install
16 | # create a .env file and put there the API KEY for retrieving data from https://weatherstack.com/
17 | $ echo "REACT_APP_API_KEY=" > .env
18 | # Start the application
19 | $ yarn start
20 | ```
21 |
22 | You can then access the app on : http://localhost:3000/
--------------------------------------------------------------------------------
/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.3.2",
8 | "@testing-library/user-event": "^7.1.2",
9 | "axios": "^0.21.1",
10 | "dotenv": "^8.2.0",
11 | "react": "^16.13.1",
12 | "react-dom": "^16.13.1",
13 | "react-scripts": "3.4.1"
14 | },
15 | "scripts": {
16 | "start": "react-scripts start",
17 | "build": "react-scripts build",
18 | "test": "react-scripts test",
19 | "eject": "react-scripts eject"
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 | }
37 |
--------------------------------------------------------------------------------
/part2/countries/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anancarv/fullstackopen/3b12bbb3f9ca4ca8c5b9c29df558d787e1009e8c/part2/countries/public/favicon.ico
--------------------------------------------------------------------------------
/part2/countries/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anancarv/fullstackopen/3b12bbb3f9ca4ca8c5b9c29df558d787e1009e8c/part2/countries/public/logo192.png
--------------------------------------------------------------------------------
/part2/countries/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anancarv/fullstackopen/3b12bbb3f9ca4ca8c5b9c29df558d787e1009e8c/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 Content from './components/Content'
4 | import Filter from './components/Filter'
5 |
6 | const App = () => {
7 | const [countries, setCountries] = useState([])
8 | const [allCountries, setAllCountries] = useState([])
9 | const [newFilter, setNewFilter] = useState('')
10 |
11 | useEffect(() => {
12 | axios
13 | .get('https://restcountries.eu/rest/v2/all')
14 | .then(response => {
15 | console.log('promise fulfilled')
16 | setAllCountries(response.data)
17 | })
18 | }, [])
19 |
20 | const handleFilterChange = (event) => {
21 | setNewFilter(event.target.value)
22 | if (newFilter) {
23 | const regex = new RegExp( newFilter, 'i' );
24 | const filteredCountries = () => allCountries.filter(country => country.name.match(regex))
25 | setCountries(filteredCountries)
26 | }
27 | }
28 |
29 | return (
30 |
31 |
32 |
33 |
34 | )
35 | }
36 |
37 | export default App
--------------------------------------------------------------------------------
/part2/countries/src/components/Content.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Country from './Country'
3 |
4 | const Content = ({countries, setCountries}) => {
5 | if (countries.length > 10) {
6 | return (
7 |
8 | Too many matches, specify another filter
9 |
10 | )
11 | } else if ((countries.length > 2 && countries.length < 10) || countries.length === 0) {
12 | return (
13 |
14 | {countries.map((country, i) =>
15 | {country.name} setCountries([country])}>show
16 | )}
17 |
18 | )
19 | } else {
20 | return (
21 |
22 | )
23 | }
24 | }
25 |
26 | export default Content
--------------------------------------------------------------------------------
/part2/countries/src/components/Filter.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const Filter = ({value, onChange}) =>
4 |
5 | find countries
6 |
7 |
8 | export default Filter
--------------------------------------------------------------------------------
/part2/countries/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/part2/countries/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import App from './App'
4 |
5 |
6 | ReactDOM.render( , document.getElementById('root'))
--------------------------------------------------------------------------------
/part2/countries/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom/extend-expect';
6 |
--------------------------------------------------------------------------------
/part2/coursecontents/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/part2/coursecontents/README.md:
--------------------------------------------------------------------------------
1 | # Course contents
2 |
3 | Simple web app for getting used to collections & modules
4 |
5 | ## Start the application
6 |
7 | To start an application, do the following :
8 |
9 | ```bash
10 | # Install dependancies
11 | $ yarn install
12 | # Start the application
13 | $ yarn start
14 | ```
15 |
16 | You can then access the app on : [http://localhost:3000/](http://localhost:3000/)
--------------------------------------------------------------------------------
/part2/coursecontents/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "coursecontents",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^4.2.4",
7 | "@testing-library/react": "^9.3.2",
8 | "@testing-library/user-event": "^7.1.2",
9 | "react": "^16.13.1",
10 | "react-dom": "^16.13.1",
11 | "react-scripts": "3.4.1"
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/coursecontents/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anancarv/fullstackopen/3b12bbb3f9ca4ca8c5b9c29df558d787e1009e8c/part2/coursecontents/public/favicon.ico
--------------------------------------------------------------------------------
/part2/coursecontents/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anancarv/fullstackopen/3b12bbb3f9ca4ca8c5b9c29df558d787e1009e8c/part2/coursecontents/public/logo192.png
--------------------------------------------------------------------------------
/part2/coursecontents/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anancarv/fullstackopen/3b12bbb3f9ca4ca8c5b9c29df558d787e1009e8c/part2/coursecontents/public/logo512.png
--------------------------------------------------------------------------------
/part2/coursecontents/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/coursecontents/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/part2/coursecontents/src/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Course from './components/Course'
3 |
4 | const App = ({courses}) =>
5 |
6 |
7 |
8 |
9 | export default App
--------------------------------------------------------------------------------
/part2/coursecontents/src/components/Content.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Part from './Part'
3 |
4 | const Content = ({parts}) =>
5 |
6 | {parts.map((part, i) =>
7 |
8 | )}
9 |
10 |
11 | export default Content
--------------------------------------------------------------------------------
/part2/coursecontents/src/components/Course.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Header from './Header'
3 | import Content from './Content'
4 | import Total from './Total'
5 |
6 | const Course = ({courses}) =>
7 |
8 | {courses.map(course =>
9 |
10 |
11 |
12 |
13 |
14 | )}
15 |
16 |
17 | export default Course
--------------------------------------------------------------------------------
/part2/coursecontents/src/components/Header.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const Header = ({course}) =>
4 |
5 | {course}
6 |
7 |
8 | export default Header
--------------------------------------------------------------------------------
/part2/coursecontents/src/components/Part.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const Part = ({part, exercises}) =>
4 |
5 | {part} {exercises}
6 |
7 |
8 | export default Part
--------------------------------------------------------------------------------
/part2/coursecontents/src/components/Total.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const Total = ({parts}) => {
4 | const total = parts.reduce((sum, part) => sum + part.exercises, 0)
5 |
6 | return (
7 | Number of exercises {total}
8 | )
9 | }
10 |
11 | export default Total
--------------------------------------------------------------------------------
/part2/coursecontents/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/part2/coursecontents/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import App from './App'
4 |
5 | const courses = [
6 | {
7 | name: 'Half Stack application development',
8 | id: 1,
9 | parts: [
10 | {
11 | name: 'Fundamentals of React',
12 | exercises: 10,
13 | id: 1
14 | },
15 | {
16 | name: 'Using props to pass data',
17 | exercises: 7,
18 | id: 2
19 | },
20 | {
21 | name: 'State of a component',
22 | exercises: 14,
23 | id: 3
24 | },
25 | {
26 | name: 'Redux',
27 | exercises: 11,
28 | id: 4
29 | }
30 | ]
31 | },
32 | {
33 | name: 'Node.js',
34 | id: 2,
35 | parts: [
36 | {
37 | name: 'Routing',
38 | exercises: 3,
39 | id: 1
40 | },
41 | {
42 | name: 'Middlewares',
43 | exercises: 7,
44 | id: 2
45 | }
46 | ]
47 | }
48 | ]
49 |
50 | ReactDOM.render( , document.getElementById('root'))
--------------------------------------------------------------------------------
/part2/coursecontents/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom/extend-expect';
6 |
--------------------------------------------------------------------------------
/part2/phonebook/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/part2/phonebook/README.md:
--------------------------------------------------------------------------------
1 | # Phonebook
2 |
3 | In this exercise, we created a simple phonebook.
4 |
5 | In this phonebook, users have the possibility to a add, update & delete a person as well as its phone number. Person's names are unique, which means that users cannot add names that already exist in the phonebook. A search field is also available in the app to filter the people by their name.
6 |
7 | This initial state of the application is stored in a file `db.json`, which correspond to a list of users along with their numbers. This file is used by the tool `JSON Server` that acts as a backend server where the data are stored.
8 |
9 | ## Start the application
10 |
11 | To start an application, do the following :
12 |
13 | ```bash
14 | # Install dependancies
15 | $ yarn install
16 | # Start the JSON Server
17 | $ npx json-server --port 3001 --watch db.json
18 | # On another terminal, start the application
19 | $ yarn start
20 | ```
21 |
22 | You can then access the app on : http://localhost:3000/
23 |
24 | You can also see the content of the JSON Server by heading to http://localhost:3001/persons
--------------------------------------------------------------------------------
/part2/phonebook/db.json:
--------------------------------------------------------------------------------
1 | {
2 | "persons": [
3 | {
4 | "name": "Zlatan ibra",
5 | "number": "46-643-463",
6 | "id": 5
7 | },
8 | {
9 | "name": "ToTorz",
10 | "number": "56-443-63",
11 | "id": 7
12 | },
13 | {
14 | "name": "Jean Dujardin",
15 | "number": "86-865-8656",
16 | "id": 8
17 | },
18 | {
19 | "name": "totoro",
20 | "number": "64-635-8375",
21 | "id": 9
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.3.2",
8 | "@testing-library/user-event": "^7.1.2",
9 | "axios": "^0.21.1",
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/anancarv/fullstackopen/3b12bbb3f9ca4ca8c5b9c29df558d787e1009e8c/part2/phonebook/public/favicon.ico
--------------------------------------------------------------------------------
/part2/phonebook/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anancarv/fullstackopen/3b12bbb3f9ca4ca8c5b9c29df558d787e1009e8c/part2/phonebook/public/logo192.png
--------------------------------------------------------------------------------
/part2/phonebook/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anancarv/fullstackopen/3b12bbb3f9ca4ca8c5b9c29df558d787e1009e8c/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/Content.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Person from './Person'
3 |
4 | const Content = ({persons, allPersons, deletePerson}) => {
5 | console.log(persons.length)
6 | if (persons.length === 0) {
7 | return (
8 |
9 | {allPersons.map((person, i) =>
10 |
11 | )}
12 |
13 | )
14 | } else {
15 | return (
16 |
17 | {persons.map((person, i) =>
18 |
19 | )}
20 |
21 | )
22 | }
23 | }
24 |
25 | export default Content
--------------------------------------------------------------------------------
/part2/phonebook/src/components/Filter.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const Filter = ({value, onChange}) =>
4 |
5 | filter shown with
6 |
7 |
8 | export default Filter
--------------------------------------------------------------------------------
/part2/phonebook/src/components/Notification.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const successStyle = {
4 | color: 'green',
5 | background: 'lightgrey',
6 | font_size: 20,
7 | border_style: 'solid',
8 | border_radius: 5,
9 | padding: 10,
10 | margin_bottom: 10
11 | }
12 |
13 | const errorStyle = {
14 | color: 'red',
15 | background: 'lightgrey',
16 | font_size: 20,
17 | border_style: 'solid',
18 | border_radius: 5,
19 | padding: 10,
20 | margin_bottom: 10
21 | }
22 |
23 | const Notification = ({ message }) => {
24 | if (message === null) {
25 | return null
26 | }
27 |
28 | if (message.includes('ERROR')){
29 | return (
30 |
31 | {message}
32 |
33 | )
34 | } else {
35 | return (
36 |
37 | {message}
38 |
39 | )
40 | }
41 | }
42 |
43 | export default Notification
--------------------------------------------------------------------------------
/part2/phonebook/src/components/Person.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const Person = ({person, deletePerson}) =>
4 |
5 | {person.name} {person.number} deletePerson(person.id)}>delete
6 |
7 |
8 | export default Person
--------------------------------------------------------------------------------
/part2/phonebook/src/components/PersonForm.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const PersonForm = ({onSubmit, newName, handleNameChange, newNumber, handleNumberChange}) =>
4 |
15 |
16 | export default PersonForm
--------------------------------------------------------------------------------
/part2/phonebook/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/part2/phonebook/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import App from './App'
4 |
5 |
6 | ReactDOM.render( , document.getElementById('root'))
--------------------------------------------------------------------------------
/part2/phonebook/src/services/persons.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | const baseUrl = '/api/persons'
3 |
4 | const getAll = () => {
5 | const request = axios.get(baseUrl)
6 | return request.then(response => response.data)
7 | }
8 |
9 | const create = newObject => {
10 | const request = axios.post(baseUrl, newObject)
11 | return request.then(response => response.data)
12 | }
13 |
14 | const update = (id, newObject) => {
15 | const request = axios.put(`${baseUrl}/${id}`, newObject)
16 | return request.then(response => response.data)
17 | }
18 |
19 | const remove = id => {
20 | const request = axios.delete(`${baseUrl}/${id}`)
21 | return request.then(response => response.data)
22 | }
23 |
24 |
25 | export default { getAll, create, update, remove }
--------------------------------------------------------------------------------
/part2/phonebook/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom/extend-expect';
6 |
--------------------------------------------------------------------------------
/part3/README.md:
--------------------------------------------------------------------------------
1 | # Solutions for part 3 exercises
2 |
3 | In this part our focus shifts towards the backend, that is, towards implementing functionality on the server side of the stack. We will implement a simple REST API in Node.js by using the Express library, and the application's data will be stored in a MongoDB database. At the end of this part, we will deploy a `phonebook` application to the internet.
4 |
--------------------------------------------------------------------------------
/part3/phonebook/.eslintignore:
--------------------------------------------------------------------------------
1 | build
--------------------------------------------------------------------------------
/part3/phonebook/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "node": true,
4 | "commonjs": true
5 | },
6 | "extends": "eslint:recommended",
7 | "globals": {
8 | "Atomics": "readonly",
9 | "SharedArrayBuffer": "readonly"
10 | },
11 | "parserOptions": {
12 | "ecmaVersion": 2018
13 | },
14 | "rules": {
15 | }
16 | }
--------------------------------------------------------------------------------
/part3/phonebook/Procfile:
--------------------------------------------------------------------------------
1 | web: node index.js
--------------------------------------------------------------------------------
/part3/phonebook/README.md:
--------------------------------------------------------------------------------
1 | # Phonebook API
2 |
3 | In this exercise, we implemented a fullstack phonebook app with a backend written in Node.js and a frontend in react.js. The data are saved in a mongodb database and the app is deployed on heroku at the folowing URL.
4 |
5 | * https://peaceful-depths-89341.herokuapp.com/
6 |
7 | ## Start the application locally
8 |
9 | To start an application:
10 |
11 | ```bash
12 | # Install dependancies
13 | $ npm install
14 |
15 | # create a .env file and put there the MONGODB_URI for connecting to your mongodb database
16 | $ echo "MONGODB_URI=" > .env
17 |
18 | # Start the application
19 | $ npm run dev
20 | ```
21 |
22 | You can then access the app on : http://localhost:3001/
--------------------------------------------------------------------------------
/part3/phonebook/build/asset-manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": {
3 | "main.js": "/static/js/main.2bb538bb.chunk.js",
4 | "main.js.map": "/static/js/main.2bb538bb.chunk.js.map",
5 | "runtime-main.js": "/static/js/runtime-main.4464e187.js",
6 | "runtime-main.js.map": "/static/js/runtime-main.4464e187.js.map",
7 | "static/js/2.d50c3fcf.chunk.js": "/static/js/2.d50c3fcf.chunk.js",
8 | "static/js/2.d50c3fcf.chunk.js.map": "/static/js/2.d50c3fcf.chunk.js.map",
9 | "index.html": "/index.html",
10 | "precache-manifest.7421c121b71b8f1dc271af0189e73b64.js": "/precache-manifest.7421c121b71b8f1dc271af0189e73b64.js",
11 | "service-worker.js": "/service-worker.js",
12 | "static/js/2.d50c3fcf.chunk.js.LICENSE.txt": "/static/js/2.d50c3fcf.chunk.js.LICENSE.txt"
13 | },
14 | "entrypoints": [
15 | "static/js/runtime-main.4464e187.js",
16 | "static/js/2.d50c3fcf.chunk.js",
17 | "static/js/main.2bb538bb.chunk.js"
18 | ]
19 | }
--------------------------------------------------------------------------------
/part3/phonebook/build/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anancarv/fullstackopen/3b12bbb3f9ca4ca8c5b9c29df558d787e1009e8c/part3/phonebook/build/favicon.ico
--------------------------------------------------------------------------------
/part3/phonebook/build/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anancarv/fullstackopen/3b12bbb3f9ca4ca8c5b9c29df558d787e1009e8c/part3/phonebook/build/logo192.png
--------------------------------------------------------------------------------
/part3/phonebook/build/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anancarv/fullstackopen/3b12bbb3f9ca4ca8c5b9c29df558d787e1009e8c/part3/phonebook/build/logo512.png
--------------------------------------------------------------------------------
/part3/phonebook/build/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 |
--------------------------------------------------------------------------------
/part3/phonebook/build/precache-manifest.7421c121b71b8f1dc271af0189e73b64.js:
--------------------------------------------------------------------------------
1 | self.__precacheManifest = (self.__precacheManifest || []).concat([
2 | {
3 | "revision": "8ff808e8c2513df31fdbbe19a8079c0a",
4 | "url": "/index.html"
5 | },
6 | {
7 | "revision": "a2c56da8308a66b9529d",
8 | "url": "/static/js/2.d50c3fcf.chunk.js"
9 | },
10 | {
11 | "revision": "e88a3e95b5364d46e95b35ae8c0dc27d",
12 | "url": "/static/js/2.d50c3fcf.chunk.js.LICENSE.txt"
13 | },
14 | {
15 | "revision": "4224683f30e0d20f0748",
16 | "url": "/static/js/main.2bb538bb.chunk.js"
17 | },
18 | {
19 | "revision": "9d2b4f7262278411ffe7",
20 | "url": "/static/js/runtime-main.4464e187.js"
21 | }
22 | ]);
--------------------------------------------------------------------------------
/part3/phonebook/build/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/part3/phonebook/build/service-worker.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Welcome to your Workbox-powered service worker!
3 | *
4 | * You'll need to register this file in your web app and you should
5 | * disable HTTP caching for this file too.
6 | * See https://goo.gl/nhQhGp
7 | *
8 | * The rest of the code is auto-generated. Please don't update this file
9 | * directly; instead, make changes to your Workbox build configuration
10 | * and re-run your build process.
11 | * See https://goo.gl/2aRDsh
12 | */
13 |
14 | importScripts("https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js");
15 |
16 | importScripts(
17 | "/precache-manifest.7421c121b71b8f1dc271af0189e73b64.js"
18 | );
19 |
20 | self.addEventListener('message', (event) => {
21 | if (event.data && event.data.type === 'SKIP_WAITING') {
22 | self.skipWaiting();
23 | }
24 | });
25 |
26 | workbox.core.clientsClaim();
27 |
28 | /**
29 | * The workboxSW.precacheAndRoute() method efficiently caches and responds to
30 | * requests for URLs in the manifest.
31 | * See https://goo.gl/S9QRab
32 | */
33 | self.__precacheManifest = [].concat(self.__precacheManifest || []);
34 | workbox.precaching.precacheAndRoute(self.__precacheManifest, {});
35 |
36 | workbox.routing.registerNavigationRoute(workbox.precaching.getCacheKeyForURL("/index.html"), {
37 |
38 | blacklist: [/^\/_/,/\/[^/?]+\.[^/]+$/],
39 | });
40 |
--------------------------------------------------------------------------------
/part3/phonebook/build/static/js/2.d50c3fcf.chunk.js.LICENSE.txt:
--------------------------------------------------------------------------------
1 | /*
2 | object-assign
3 | (c) Sindre Sorhus
4 | @license MIT
5 | */
6 |
7 | /** @license React v0.19.1
8 | * scheduler.production.min.js
9 | *
10 | * Copyright (c) Facebook, Inc. and its affiliates.
11 | *
12 | * This source code is licensed under the MIT license found in the
13 | * LICENSE file in the root directory of this source tree.
14 | */
15 |
16 | /** @license React v16.13.1
17 | * react-dom.production.min.js
18 | *
19 | * Copyright (c) Facebook, Inc. and its affiliates.
20 | *
21 | * This source code is licensed under the MIT license found in the
22 | * LICENSE file in the root directory of this source tree.
23 | */
24 |
25 | /** @license React v16.13.1
26 | * react.production.min.js
27 | *
28 | * Copyright (c) Facebook, Inc. and its affiliates.
29 | *
30 | * This source code is licensed under the MIT license found in the
31 | * LICENSE file in the root directory of this source tree.
32 | */
33 |
--------------------------------------------------------------------------------
/part3/phonebook/build/static/js/runtime-main.4464e187.js:
--------------------------------------------------------------------------------
1 | !function(e){function r(r){for(var n,l,p=r[0],f=r[1],i=r[2],c=0,s=[];c {
13 | console.log('connected to MongoDB')
14 | })
15 | .catch((error) => {
16 | console.log('error connecting to MongoDB:', error.message)
17 | })
18 |
19 | const personSchema = new mongoose.Schema({
20 | name: {
21 | type: String,
22 | minlength: 3,
23 | required: true,
24 | unique: true
25 | },
26 | number: {
27 | type: String,
28 | minlength: 8,
29 | required: true
30 | }
31 | })
32 |
33 | personSchema.set('toJSON', {
34 | transform: (document, returnedObject) => {
35 | returnedObject.id = returnedObject._id.toString()
36 | delete returnedObject._id
37 | delete returnedObject.__v
38 | }
39 | })
40 | personSchema.plugin(uniqueValidator);
41 |
42 | module.exports = mongoose.model('Person', personSchema)
--------------------------------------------------------------------------------
/part3/phonebook/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "phonebook-api",
3 | "version": "1.0.0",
4 | "description": "Simple REST API with NodeJS and Express",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "node index.js",
8 | "dev": "nodemon index.js",
9 | "test": "echo \"Error: no test specified\" && exit 1",
10 | "lint": "eslint ."
11 | },
12 | "author": "Ananias CARVALHO ",
13 | "license": "ISC",
14 | "dependencies": {
15 | "cors": "^2.8.5",
16 | "dotenv": "^8.2.0",
17 | "express": "^4.17.1",
18 | "mongoose": "^5.9.7",
19 | "mongoose-unique-validator": "^2.0.3",
20 | "morgan": "^1.10.0"
21 | },
22 | "devDependencies": {
23 | "eslint": "^6.8.0",
24 | "nodemon": "^2.0.2"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/part4/README.md:
--------------------------------------------------------------------------------
1 | # Solutions for part 4 exercises
2 |
3 | In this part, we will continue our work on the backend. Our first major theme will be writing unit and integration tests for the backend. After we have covered testing, we will take a look at implementing user authentication and authorization.
--------------------------------------------------------------------------------
/part4/bloglist/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "commonjs": true,
5 | "es6": true,
6 | "node": true,
7 | "jest": true
8 | },
9 | "extends": "eslint:recommended",
10 | "globals": {
11 | "Atomics": "readonly",
12 | "SharedArrayBuffer": "readonly"
13 | },
14 | "parserOptions": {
15 | "ecmaVersion": 2018
16 | },
17 | "rules": {
18 | }
19 | }
--------------------------------------------------------------------------------
/part4/bloglist/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # misc
12 | .DS_Store
13 | .env.local
14 | .env.development.local
15 | .env.test.local
16 | .env.production.local
17 |
18 | npm-debug.log*
19 | yarn-debug.log*
20 | yarn-error.log*
21 |
--------------------------------------------------------------------------------
/part4/bloglist/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 | const body = request.body
8 |
9 | const user = await User.findOne({ username: body.username })
10 | const passwordCorrect = user === null
11 | ? false
12 | : await bcrypt.compare(body.password, user.passwordHash)
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 | })
31 |
32 | module.exports = loginRouter
--------------------------------------------------------------------------------
/part4/bloglist/controllers/testing.js:
--------------------------------------------------------------------------------
1 | const testingRouter = require('express').Router()
2 | const Blog = require('../models/blog')
3 | const User = require('../models/user')
4 |
5 | testingRouter.post('/reset', async (request, response) => {
6 | await Blog.deleteMany({})
7 | await User.deleteMany({})
8 |
9 | response.status(204).end()
10 | })
11 |
12 | module.exports = testingRouter
--------------------------------------------------------------------------------
/part4/bloglist/controllers/users.js:
--------------------------------------------------------------------------------
1 | const bcrypt = require('bcrypt')
2 | const usersRouter = require('express').Router()
3 | const User = require('../models/user')
4 |
5 | usersRouter.post('/', async (request, response) => {
6 | const body = request.body
7 |
8 | if (body.password.length < 3) {
9 | return response.status(400).json({ error: `User validation failed: username: Path password is shorter than the minimum allowed length (3)` })
10 | }
11 |
12 | const saltRounds = 10
13 | const passwordHash = await bcrypt.hash(body.password, saltRounds)
14 |
15 | const user = new User({
16 | username: body.username,
17 | name: body.name,
18 | passwordHash,
19 | })
20 |
21 | const savedUser = await user.save()
22 |
23 | response.json(savedUser)
24 | })
25 |
26 | usersRouter.get('/', async (request, response) => {
27 | const users = await User.find({}).populate('blogs', { url: 1, title: 1, author: 1 })
28 | response.json(users.map(user => user.toJSON()))
29 | })
30 |
31 | module.exports = usersRouter
--------------------------------------------------------------------------------
/part4/bloglist/index.js:
--------------------------------------------------------------------------------
1 | const app = require('./app')
2 | const http = require('http')
3 | const config = require('./utils/config')
4 | const logger = require('./utils/logger')
5 |
6 | const server = http.createServer(app)
7 |
8 |
9 | server.listen(config.PORT, () => {
10 | logger.info(`Server running on port ${config.PORT}`)
11 | })
--------------------------------------------------------------------------------
/part4/bloglist/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | testEnvironment: 'node',
3 | setupFilesAfterEnv: [
4 | "./jest.setup.js"
5 | ]
6 | }
--------------------------------------------------------------------------------
/part4/bloglist/jest.setup.js:
--------------------------------------------------------------------------------
1 | jest.setTimeout(30000)
--------------------------------------------------------------------------------
/part4/bloglist/models/blog.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose')
2 | const uniqueValidator = require('mongoose-unique-validator');
3 |
4 | mongoose.set('useFindAndModify', false)
5 | mongoose.set('useCreateIndex', true)
6 |
7 | const blogSchema = mongoose.Schema({
8 | title: {
9 | type: String,
10 | minlength: 3,
11 | required: true,
12 | unique: true
13 | },
14 | author: {
15 | type: String,
16 | required: true
17 | },
18 | url: {
19 | type: String,
20 | minlength: 3,
21 | required: true
22 | },
23 | likes: {
24 | type: Number
25 | },
26 | comments: [{
27 | type: String
28 | }],
29 | user: {
30 | type: mongoose.Schema.Types.ObjectId,
31 | ref: 'User'
32 | },
33 | })
34 |
35 |
36 | blogSchema.set('toJSON', {
37 | transform: (document, returnedObject) => {
38 | returnedObject.id = returnedObject._id.toString()
39 | delete returnedObject._id
40 | delete returnedObject.__v
41 | }
42 | })
43 | blogSchema.plugin(uniqueValidator);
44 |
45 | module.exports = mongoose.model('Blog', blogSchema)
--------------------------------------------------------------------------------
/part4/bloglist/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 | required: true,
8 | minlength: 3,
9 | unique: true
10 | },
11 | name: String,
12 | passwordHash: {
13 | type: String,
14 | required: true
15 | },
16 | blogs: [
17 | {
18 | type: mongoose.Schema.Types.ObjectId,
19 | ref: 'Blog'
20 | }
21 | ],
22 | })
23 |
24 | userSchema.set('toJSON', {
25 | transform: (document, returnedObject) => {
26 | returnedObject.id = returnedObject._id.toString()
27 | delete returnedObject._id
28 | delete returnedObject.__v
29 | // the passwordHash should not be revealed
30 | delete returnedObject.passwordHash
31 | }
32 | })
33 |
34 | userSchema.plugin(uniqueValidator)
35 |
36 | module.exports = mongoose.model('User', userSchema)
37 |
--------------------------------------------------------------------------------
/part4/bloglist/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "bloglist",
3 | "version": "1.0.0",
4 | "description": "Blog list application",
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 | "lint": "eslint .",
10 | "test": "cross-env NODE_ENV=test jest --verbose --runInBand",
11 | "start:test": "cross-env NODE_ENV=test node index.js"
12 | },
13 | "author": "Ananias CARVALHO ",
14 | "license": "ISC",
15 | "devDependencies": {
16 | "eslint": "^6.8.0",
17 | "jest": "^25.3.0",
18 | "nodemon": "^2.0.2",
19 | "supertest": "^4.0.2"
20 | },
21 | "dependencies": {
22 | "bcrypt": "^5.0.0",
23 | "body-parser": "^1.19.0",
24 | "cors": "^2.8.5",
25 | "cross-env": "^7.0.2",
26 | "dotenv": "^8.2.0",
27 | "express": "^4.17.1",
28 | "express-async-errors": "^3.1.1",
29 | "jsonwebtoken": "^8.5.1",
30 | "mongoose": "^5.9.7",
31 | "mongoose-unique-validator": "^2.0.3",
32 | "morgan": "^1.10.0"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/part4/bloglist/tests/test_helper.js:
--------------------------------------------------------------------------------
1 | const Blog = require('../models/blog')
2 | const User = require('../models/user')
3 |
4 | const initialBlogs = [
5 | {
6 | id:"5a422a851b54a676234d17f7",
7 | title:"React patterns",
8 | author:"Michael Chan",
9 | url:"https://reactpatterns.com/",
10 | likes:7
11 | },
12 | {
13 | id:"5a422aa71b54a676234d17f8",
14 | title:"Go To Statement Considered Harmful",
15 | author:"Edsger W. Dijkstra",
16 | url:"http://www.u.arizona.edu/~rubinson/copyright_violations/Go_To_Considered_Harmful.html",
17 | likes:5
18 | }
19 | ]
20 |
21 | const blogsInDb = async () => {
22 | const blogs = await Blog.find({})
23 | return blogs.map(blog => blog.toJSON())
24 | }
25 |
26 | const usersInDb = async () => {
27 | const users = await User.find({})
28 | return users.map(u => u.toJSON())
29 | }
30 |
31 | module.exports = {
32 | initialBlogs, blogsInDb, usersInDb
33 | }
--------------------------------------------------------------------------------
/part4/bloglist/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 | } else if (process.env.NODE_ENV === 'development') {
9 | MONGODB_URI = process.env.DEV_MONGODB_URI
10 | }
11 |
12 | module.exports = {
13 | MONGODB_URI,
14 | PORT
15 | }
--------------------------------------------------------------------------------
/part4/bloglist/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 | if (process.env.NODE_ENV !== 'test') {
9 | console.error(...params)
10 | }
11 | }
12 |
13 | module.exports = { info, error }
--------------------------------------------------------------------------------
/part4/bloglist/utils/middleware.js:
--------------------------------------------------------------------------------
1 | const logger = require('./logger')
2 | const jwt = require('jsonwebtoken')
3 |
4 | const errorHandler = (error, request, response, next) => {
5 | logger.error(error.message)
6 |
7 | if (error.name === 'CastError') {
8 | return response.status(400).send({ error: 'malformatted id' })
9 | } else if (error.name === 'ValidationError') {
10 | return response.status(400).json({ error: error.message })
11 | } else if (error.name === 'JsonWebTokenError') {
12 | return response.status(401).json({
13 | error: 'invalid token'
14 | })
15 | }
16 |
17 | logger.error(error.message)
18 | next(error)
19 | }
20 |
21 | const tokenExtractor = (request, response, next) => {
22 | const authorization = request.get('authorization')
23 |
24 | if (authorization && authorization.toLowerCase().startsWith('bearer ')) {
25 | request["token"] = authorization.substring(7)
26 | }
27 | next()
28 | }
29 |
30 | const tokenValidator = (request, response, next) => {
31 | const token = request.token
32 | if (!token) {
33 | return response.status(401).json({ error: 'token missing' })
34 | }
35 |
36 | const decodedToken = jwt.verify(token, process.env.SECRET)
37 | if (!decodedToken.id) {
38 | return response.status(401).json({ error: 'invalid token' })
39 | }
40 | next()
41 | }
42 |
43 | module.exports = { errorHandler, tokenExtractor, tokenValidator }
--------------------------------------------------------------------------------
/part5/README.md:
--------------------------------------------------------------------------------
1 | # Solutions for part 5 exercises
2 |
3 | In this part we return to the frontend, first looking at different possibilities for testing the React code. We will also implement token based authentication which will enable users to log in to our application.
--------------------------------------------------------------------------------
/part5/bloglist-frontend/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig helps developers define and maintain consistent
2 | # coding styles between different editors and IDEs
3 | # editorconfig.org
4 |
5 | root = true
6 |
7 | [*]
8 |
9 | # Change these settings to your own preference
10 | indent_style = space
11 | indent_size = 2
12 |
13 | # We recommend you to keep these unchanged
14 | end_of_line = lf
15 | charset = utf-8
16 | trim_trailing_whitespace = true
17 | insert_final_newline = true
18 |
19 | [*.md]
20 | trim_trailing_whitespace = false
--------------------------------------------------------------------------------
/part5/bloglist-frontend/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | build
--------------------------------------------------------------------------------
/part5/bloglist-frontend/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "env": {
3 | "browser": true,
4 | "es6": true,
5 | "jest/globals": true
6 | },
7 | "extends": [
8 | "eslint:recommended",
9 | "plugin:react/recommended"
10 | ],
11 | "parserOptions": {
12 | "ecmaFeatures": {
13 | "jsx": true
14 | },
15 | "ecmaVersion": 2018,
16 | "sourceType": "module"
17 | },
18 | "plugins": [
19 | "react", "jest"
20 | ],
21 | "rules": {
22 | "indent": [
23 | "error",
24 | 2
25 | ],
26 | "quotes": [
27 | "error",
28 | "single"
29 | ],
30 | "semi": [
31 | "error",
32 | "never"
33 | ],
34 | "eqeqeq": "error",
35 | "no-trailing-spaces": "error",
36 | "object-curly-spacing": [
37 | "error", "always"
38 | ],
39 | "arrow-spacing": [
40 | "error", { "before": true, "after": true }
41 | ],
42 | "no-console": 0,
43 | "react/prop-types": 0
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/part5/bloglist-frontend/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/part5/bloglist-frontend/README.md:
--------------------------------------------------------------------------------
1 | # Bloglist frontend
2 |
3 | In this exercise, we will now create a frontend for the bloglist backend we created in the last part.
4 | A login functionnality is also implemented for restricting the possibility to view and create blogs only by authenticated users. We assume that a user already exists with the good credentials.
5 |
6 | Since the objective of this part is to test the react app, unit tests and end-to-end (E2E) tests with cypress are also implemented.
7 |
8 | ## Start the application locally
9 |
10 | To start an application:
11 |
12 | ```bash
13 | # First, you need to start the backend, to do so, head to the part4. Everything is explained in the README
14 |
15 | # Install dependancies
16 | $ npm install
17 |
18 | # Start the frontend application
19 | $ npm start
20 |
21 | # For running E2E tests
22 | $ npm run cypress:open # Then, click on run all specs
23 | ```
24 |
25 | You can then access the app on : http://localhost:3000/
--------------------------------------------------------------------------------
/part5/bloglist-frontend/cypress.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/part5/bloglist-frontend/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/bloglist-frontend/cypress/plugins/index.js:
--------------------------------------------------------------------------------
1 | ///
2 | // ***********************************************************
3 | // This example plugins/index.js can be used to load plugins
4 | //
5 | // You can change the location of this file or turn off loading
6 | // the plugins file with the 'pluginsFile' configuration option.
7 | //
8 | // You can read more here:
9 | // https://on.cypress.io/plugins-guide
10 | // ***********************************************************
11 |
12 | // This function is called when a project is opened or re-opened (e.g. due to
13 | // the project's config changing)
14 |
15 | /**
16 | * @type {Cypress.PluginConfig}
17 | */
18 | module.exports = (on, config) => {
19 | // `on` is used to hook into various events Cypress emits
20 | // `config` is the resolved Cypress config
21 | }
22 |
--------------------------------------------------------------------------------
/part5/bloglist-frontend/cypress/support/commands.js:
--------------------------------------------------------------------------------
1 | const storageKey = 'loggedBlogappUser'
2 |
3 | Cypress.Commands.add('login', ({ username, password }) => {
4 | cy.request('POST', 'http://localhost:3001/api/login', {
5 | username, password
6 | }).then(({ body }) => {
7 | localStorage.setItem(storageKey, JSON.stringify(body))
8 | cy.visit('http://localhost:3000')
9 | })
10 | })
11 |
12 | Cypress.Commands.add('createBlog', ({ title, author, url }) => {
13 | cy.request({
14 | url: 'http://localhost:3001/api/blogs',
15 | method: 'POST',
16 | body: { title, author, url },
17 | headers: {
18 | 'Authorization': `bearer ${JSON.parse(localStorage.getItem(storageKey)).token}`
19 | }
20 | })
21 |
22 | cy.visit('http://localhost:3000')
23 | })
24 |
--------------------------------------------------------------------------------
/part5/bloglist-frontend/cypress/support/index.js:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example support/index.js is processed and
3 | // loaded automatically before your test files.
4 | //
5 | // This is a great place to put global configuration and
6 | // behavior that modifies Cypress.
7 | //
8 | // You can change the location of this file or turn off
9 | // automatically serving support files with the
10 | // 'supportFile' configuration option.
11 | //
12 | // You can read more here:
13 | // https://on.cypress.io/configuration
14 | // ***********************************************************
15 |
16 | // Import commands.js using ES2015 syntax:
17 | import './commands'
18 |
19 | // Alternatively you can use CommonJS syntax:
20 | // require('./commands')
21 |
--------------------------------------------------------------------------------
/part5/bloglist-frontend/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.21.1",
8 | "prop-types": "^15.7.2",
9 | "react": "^16.12.0",
10 | "react-dom": "^16.12.0",
11 | "react-scripts": "3.3.1"
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 | "cypress:open": "cypress open"
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 | "proxy": "http://localhost:3001",
37 | "devDependencies": {
38 | "@testing-library/jest-dom": "^4.2.4",
39 | "@testing-library/react": "^9.5.0",
40 | "cypress": "^4.4.0",
41 | "eslint-plugin-jest": "^23.8.2",
42 | "prettier": "2.0.4"
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/part5/bloglist-frontend/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anancarv/fullstackopen/3b12bbb3f9ca4ca8c5b9c29df558d787e1009e8c/part5/bloglist-frontend/public/favicon.ico
--------------------------------------------------------------------------------
/part5/bloglist-frontend/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anancarv/fullstackopen/3b12bbb3f9ca4ca8c5b9c29df558d787e1009e8c/part5/bloglist-frontend/public/logo192.png
--------------------------------------------------------------------------------
/part5/bloglist-frontend/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anancarv/fullstackopen/3b12bbb3f9ca4ca8c5b9c29df558d787e1009e8c/part5/bloglist-frontend/public/logo512.png
--------------------------------------------------------------------------------
/part5/bloglist-frontend/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "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/bloglist-frontend/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/part5/bloglist-frontend/src/components/Blog.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import '@testing-library/jest-dom/extend-expect'
3 | import { render, fireEvent } from '@testing-library/react'
4 | import Blog from './Blog'
5 |
6 | describe('Blog component tests', () => {
7 | let blog = {
8 | title:"React patterns",
9 | author:"Michael Chan",
10 | url:"https://reactpatterns.com/",
11 | likes:7
12 | }
13 |
14 | let mockUpdateBlog = jest.fn()
15 | let mockDeleteBlog = jest.fn()
16 |
17 | test('renders title and author', () => {
18 | const component = render(
19 |
20 | )
21 | expect(component.container).toHaveTextContent(
22 | 'React patterns - Michael Chan'
23 | )
24 | })
25 |
26 | test('clicking the view button displays url and number of likes', () => {
27 | const component = render(
28 |
29 | )
30 |
31 | const button = component.getByText('view')
32 | fireEvent.click(button)
33 |
34 | expect(component.container).toHaveTextContent(
35 | 'https://reactpatterns.com/'
36 | )
37 |
38 | expect(component.container).toHaveTextContent(
39 | '7'
40 | )
41 | })
42 | })
43 |
--------------------------------------------------------------------------------
/part5/bloglist-frontend/src/components/BlogForm.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | const BlogForm = ({ createBlog }) => {
5 | const [newTitle, setNewTitle ] = useState('')
6 | const [newAuthor, setNewAuthor ] = useState('')
7 | const [newUrl, setNewUrl ] = useState('')
8 |
9 | const handleTitleChange = (event) => {
10 | setNewTitle(event.target.value)
11 | }
12 |
13 | const handleAuthorChange = (event) => {
14 | setNewAuthor(event.target.value)
15 | }
16 |
17 | const handleUrlChange = (event) => {
18 | setNewUrl(event.target.value)
19 | }
20 |
21 | const addBlog = (event) => {
22 | event.preventDefault()
23 | createBlog({
24 | title: newTitle,
25 | author: newAuthor,
26 | url: newUrl
27 | })
28 | setNewTitle('')
29 | setNewAuthor('')
30 | setNewUrl('')
31 | }
32 |
33 | return (
34 |
48 | )
49 | }
50 |
51 | BlogForm.propTypes = {
52 | createBlog: PropTypes.func.isRequired
53 | }
54 |
55 | export default BlogForm
56 |
--------------------------------------------------------------------------------
/part5/bloglist-frontend/src/components/BlogForm.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 BlogForm from './BlogForm'
5 |
6 | test(' updates parent state and calls onSubmit', () => {
7 | const createBlog = jest.fn()
8 |
9 | const component = render(
10 |
11 | )
12 |
13 | const input = component.container.querySelector('#title')
14 | const form = component.container.querySelector('form')
15 |
16 | fireEvent.change(input, {
17 | target: { value: 'Go To Statement Considered Harmful' }
18 | })
19 | fireEvent.submit(form)
20 |
21 | expect(createBlog.mock.calls).toHaveLength(1)
22 | expect(createBlog.mock.calls[0][0].title).toBe('Go To Statement Considered Harmful' )
23 | })
24 |
--------------------------------------------------------------------------------
/part5/bloglist-frontend/src/components/LoginForm.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | const LoginForm = (props) => {
5 | return (
6 |
15 | )}
16 |
17 | LoginForm.propTypes = {
18 | handleLogin: PropTypes.func.isRequired,
19 | setUsername: PropTypes.func.isRequired,
20 | setPassword: PropTypes.func.isRequired,
21 | username: PropTypes.string.isRequired,
22 | password: PropTypes.string.isRequired
23 | }
24 |
25 | export default LoginForm
26 |
--------------------------------------------------------------------------------
/part5/bloglist-frontend/src/components/Notification.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | const error = {
5 | color: 'red',
6 | background: 'lightgrey',
7 | font_size: 20,
8 | border_style: 'solid',
9 | border_radius: 5,
10 | padding: 10,
11 | margin_bottom: 10
12 | }
13 |
14 | const success = {
15 | color: 'green',
16 | background: 'lightgrey',
17 | font_size: 20,
18 | border_style: 'solid',
19 | border_radius: 5,
20 | padding: 10,
21 | margin_bottom: 10
22 | }
23 |
24 | const Notification = ({ errorMessage, successMessage }) => {
25 | if (successMessage === null && errorMessage === null) {
26 | return null
27 | } else if (successMessage){
28 | return (
29 |
30 | {successMessage}
31 |
32 | )
33 | } else {
34 | return (
35 |
36 | {errorMessage}
37 |
38 | )
39 | }
40 | }
41 |
42 | Notification.propTypes = {
43 | errorMessage: PropTypes.string,
44 | successMessage: PropTypes.string
45 | }
46 |
47 | export default Notification
48 |
--------------------------------------------------------------------------------
/part5/bloglist-frontend/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 = () => {
11 | setVisible(!visible)
12 | }
13 |
14 | useImperativeHandle(ref, () => {
15 | return {
16 | toggleVisibility
17 | }
18 | })
19 |
20 | return (
21 |
22 |
23 | {props.buttonLabel}
24 |
25 |
26 | {props.children}
27 | cancel
28 |
29 |
30 | )
31 | })
32 |
33 | Togglable.displayName = 'Togglable'
34 | Togglable.propTypes = {
35 | buttonLabel: PropTypes.string.isRequired
36 | }
37 |
38 | export default Togglable
39 |
--------------------------------------------------------------------------------
/part5/bloglist-frontend/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'))
--------------------------------------------------------------------------------
/part5/bloglist-frontend/src/services/blogs.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | const baseUrl = '/api/blogs'
3 |
4 | let token = null
5 | let config
6 |
7 | const setToken = newToken => {
8 | token = `bearer ${newToken}`
9 | config = {
10 | headers: { Authorization: token },
11 | }
12 | }
13 |
14 | const getAll = async () => {
15 | const response = await axios.get(baseUrl, config)
16 | return response.data
17 | }
18 |
19 | const create = async newObject => {
20 | const response = await axios.post(baseUrl, newObject, config)
21 | return response.data
22 | }
23 |
24 | const update = async objectToUpdate => {
25 | const response = await axios.put(`${baseUrl}/${objectToUpdate.id}`, objectToUpdate, config)
26 | return response.data
27 | }
28 |
29 | const remove = async id => {
30 | const response = await axios.delete(`${baseUrl}/${id}`, config)
31 | return response.data
32 | }
33 |
34 |
35 | export default { getAll, create, update, setToken, remove }
--------------------------------------------------------------------------------
/part5/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 }
--------------------------------------------------------------------------------
/part5/bloglist-frontend/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom/extend-expect'
6 |
--------------------------------------------------------------------------------
/part6/README.md:
--------------------------------------------------------------------------------
1 | # Solutions for part 6 exercises
2 |
3 | So far, we have placed the application's state and state logic directly inside React-components. When applications grow larger, state management should be moved outside React-components. In this part, we will introduce the Redux-library, which is currently the most popular solution for managing the state of React-applications.
--------------------------------------------------------------------------------
/part6/redux-anecdotes/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/part6/redux-anecdotes/README.md:
--------------------------------------------------------------------------------
1 | # Redux anecdoce
2 |
3 | In this exercise, we made a new version of the anecdote voting application from part 1 using Redux.
4 | As a reminder, this application allows the user to vote between multiple anecdotes and then displays the most popular one.
5 |
6 | This initial list of anecdotes is stored in the file `db.json`. This file is used by the tool `JSON Server` that acts as a backend server where the data are stored.
7 |
8 | ## Start the application
9 |
10 | To start an application, do the following :
11 |
12 | ```bash
13 | # Install dependancies
14 | $ npm install
15 |
16 | # Start the backend JSON server
17 | $ npx json-server --port 3001 --watch db.json
18 |
19 | # Start the application
20 | $ npm start
21 | ```
22 |
23 | You can then access the app on : [http://localhost:3000/](http://localhost:3000/)
24 | You can also see the content of the JSON Server by heading to http://localhost:3001/anecdotes
--------------------------------------------------------------------------------
/part6/redux-anecdotes/db.json:
--------------------------------------------------------------------------------
1 | {
2 | "anecdotes": [
3 | {
4 | "content": "Adding manpower to a late software project makes it later!",
5 | "id": "21149",
6 | "votes": 2
7 | },
8 | {
9 | "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.",
10 | "id": "69581",
11 | "votes": 2
12 | },
13 | {
14 | "content": "Any fool can write code that a computer can understand. Good programmers write code that humans can understand.",
15 | "id": "36975",
16 | "votes": 4
17 | },
18 | {
19 | "content": "Premature optimization is the root of all evil.",
20 | "id": "25170",
21 | "votes": 3
22 | },
23 | {
24 | "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.",
25 | "id": "98312",
26 | "votes": 4
27 | },
28 | {
29 | "content": "Test",
30 | "id": "26769",
31 | "votes": 3
32 | }
33 | ]
34 | }
--------------------------------------------------------------------------------
/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.21.1",
10 | "json-server": "^0.16.1",
11 | "react": "^16.12.0",
12 | "react-dom": "^16.12.0",
13 | "react-redux": "^7.1.3",
14 | "react-scripts": "3.3.1",
15 | "redux": "^4.0.5",
16 | "redux-devtools-extension": "^2.13.8",
17 | "redux-thunk": "^2.3.0"
18 | },
19 | "scripts": {
20 | "start": "react-scripts start",
21 | "build": "react-scripts build",
22 | "test": "react-scripts test",
23 | "eject": "react-scripts eject",
24 | "server": "json-server -p3001 --watch db.json"
25 | },
26 | "eslintConfig": {
27 | "extends": "react-app"
28 | },
29 | "browserslist": {
30 | "production": [
31 | ">0.2%",
32 | "not dead",
33 | "not op_mini all"
34 | ],
35 | "development": [
36 | "last 1 chrome version",
37 | "last 1 firefox version",
38 | "last 1 safari version"
39 | ]
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/part6/redux-anecdotes/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anancarv/fullstackopen/3b12bbb3f9ca4ca8c5b9c29df558d787e1009e8c/part6/redux-anecdotes/public/favicon.ico
--------------------------------------------------------------------------------
/part6/redux-anecdotes/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anancarv/fullstackopen/3b12bbb3f9ca4ca8c5b9c29df558d787e1009e8c/part6/redux-anecdotes/public/logo192.png
--------------------------------------------------------------------------------
/part6/redux-anecdotes/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anancarv/fullstackopen/3b12bbb3f9ca4ca8c5b9c29df558d787e1009e8c/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 NewAnecdote from './components/AnecdoteForm'
3 | import AnecdoteList from './components/AnecdoteList'
4 | import Notification from './components/Notification'
5 | import Filter from './components/Filter'
6 | import { initializeAnecdotes } from './reducers/anecdoteReducer'
7 | import { useDispatch } from 'react-redux'
8 |
9 | const App = () => {
10 | const dispatch = useDispatch()
11 | useEffect(() => {
12 | dispatch(initializeAnecdotes())
13 | }, [dispatch])
14 |
15 | return (
16 |
17 |
create new
18 |
19 |
Anecdotes
20 |
21 |
22 |
23 |
24 | )
25 | }
26 |
27 | export default App
--------------------------------------------------------------------------------
/part6/redux-anecdotes/src/components/AnecdoteForm.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { useDispatch } from 'react-redux'
3 | import { createAnecdote } from '../reducers/anecdoteReducer'
4 | import { setNotification } from '../reducers/notificationReducer'
5 |
6 | const NewAnecdote = () => {
7 | const dispatch = useDispatch()
8 |
9 | const addAnecdote = async (event) => {
10 | event.preventDefault()
11 | const content = event.target.anecdote.value
12 | event.target.anecdote.value = ''
13 | dispatch(createAnecdote(content))
14 | dispatch(setNotification(`Anecdote '${content}' successfully added`, 5))
15 | }
16 |
17 | return (
18 |
22 | )
23 | }
24 |
25 | export default NewAnecdote
--------------------------------------------------------------------------------
/part6/redux-anecdotes/src/components/AnecdoteList.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { useDispatch, useSelector } from 'react-redux'
3 | import { vote } from '../reducers/anecdoteReducer'
4 | import { setNotification } from '../reducers/notificationReducer'
5 |
6 | const Anecdote = ({ anecdote }) => {
7 | const dispatch = useDispatch()
8 |
9 | const voteHandler = () => {
10 | dispatch(vote(anecdote))
11 | dispatch(setNotification(`You voted for '${anecdote.content}'`, 5))
12 | }
13 |
14 | return (
15 |
16 |
17 | {anecdote.content}
18 |
19 |
20 | has {anecdote.votes}
21 | vote
22 |
23 |
24 | )
25 | }
26 |
27 | const AnecdoteList = () => {
28 | const anecdotes = useSelector(({filter, anecdotes}) => {
29 | if ( filter === null ) {
30 | return anecdotes
31 | }
32 | const regex = new RegExp( filter, 'i' )
33 | return anecdotes.filter(anecdote => anecdote.content.match(regex))
34 | })
35 |
36 | const byVotes = (b1, b2) => b2.votes - b1.votes
37 |
38 | return(
39 | anecdotes.sort(byVotes).map(anecdote => )
40 | )
41 | }
42 |
43 | export default AnecdoteList
--------------------------------------------------------------------------------
/part6/redux-anecdotes/src/components/Filter.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { useDispatch } from 'react-redux'
3 | import { filterChange } from '../reducers/filterReducer'
4 |
5 | const Filter = () => {
6 | const dispatch = useDispatch()
7 |
8 | const handleChange = (event) => {
9 | dispatch(filterChange(event.target.value))
10 | }
11 | const style = {
12 | marginBottom: 10
13 | }
14 |
15 | return (
16 |
17 | filter
18 |
19 | )
20 | }
21 |
22 | export default Filter
--------------------------------------------------------------------------------
/part6/redux-anecdotes/src/components/Notification.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { useSelector } from 'react-redux'
3 |
4 | const Notification = () => {
5 | const notification = useSelector(state => state.notification)
6 |
7 | const style = {
8 | border: 'solid',
9 | padding: 10,
10 | borderWidth: 1
11 | }
12 |
13 | if (notification === null) {
14 | return null
15 | }
16 |
17 | return (
18 |
19 | {notification}
20 |
21 | )
22 | }
23 |
24 | export default Notification
--------------------------------------------------------------------------------
/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 './utils/store'
5 | import App from './App'
6 |
7 |
8 | ReactDOM.render(
9 |
10 |
11 | ,
12 | document.getElementById('root')
13 | )
--------------------------------------------------------------------------------
/part6/redux-anecdotes/src/reducers/filterReducer.js:
--------------------------------------------------------------------------------
1 | const filterReducer = (state = null, action) => {
2 | switch (action.type) {
3 | case 'SET_FILTER':
4 | return action.filter
5 | default:
6 | return state
7 | }
8 | }
9 |
10 | export const filterChange = (filter) => {
11 | return {
12 | type: 'SET_FILTER',
13 | filter,
14 | }
15 | }
16 |
17 | export default filterReducer
--------------------------------------------------------------------------------
/part6/redux-anecdotes/src/reducers/notificationReducer.js:
--------------------------------------------------------------------------------
1 | const notificationReducer = (state = null, action) => {
2 | switch (action.type) {
3 | case 'NEW_NOTIFICATION':
4 | return action.notification
5 | case 'HIDE_NOTIFICATION':
6 | return action.notification
7 | default:
8 | return state
9 | }
10 | }
11 |
12 | export const setNotification = (notification, displayTime) => {
13 | return async dispatch => {
14 | dispatch({
15 | type: 'NEW_NOTIFICATION',
16 | notification,
17 | })
18 |
19 | setTimeout(() => {
20 | dispatch({
21 | type: 'HIDE_NOTIFICATION',
22 | notification: null
23 | })
24 | }, displayTime * 1000)
25 | }
26 | }
27 |
28 | export default notificationReducer
--------------------------------------------------------------------------------
/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 object = {
12 | content: content,
13 | id: (100000 * Math.random()).toFixed(0),
14 | votes: 0
15 | }
16 | const response = await axios.post(baseUrl, object)
17 | return response.data
18 | }
19 |
20 | const update = async objectToUpdate => {
21 | const response = await axios.put(`${baseUrl}/${objectToUpdate.id}`, objectToUpdate)
22 | return response.data
23 | }
24 |
25 | export default { getAll, createNew, update }
--------------------------------------------------------------------------------
/part6/redux-anecdotes/src/utils/store.js:
--------------------------------------------------------------------------------
1 | import { createStore, combineReducers, applyMiddleware } from 'redux'
2 | import thunk from 'redux-thunk'
3 | import { composeWithDevTools } from 'redux-devtools-extension'
4 |
5 | import anecdoteReducer from '../reducers/anecdoteReducer'
6 | import notificationReducer from '../reducers/notificationReducer'
7 | import filterReducer from '../reducers/filterReducer'
8 |
9 | const reducer = combineReducers({
10 | notification: notificationReducer,
11 | anecdotes: anecdoteReducer,
12 | filter: filterReducer
13 | })
14 |
15 | const store = createStore(
16 | reducer,
17 | composeWithDevTools(
18 | applyMiddleware(thunk)
19 | )
20 | )
21 |
22 | export default store
--------------------------------------------------------------------------------
/part6/unicafe-redux/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/part6/unicafe-redux/README.md:
--------------------------------------------------------------------------------
1 | # Unicafe Redux
2 |
3 | In this exercise, we made a simplified version of the unicafe-exercise from part 1 with handling of the state management with `Redux`. As a reminder, the unicafe app collects customer feedback with three options: good, neutral, and bad.
4 |
5 | ## Start the application
6 |
7 | To start an application, do the following :
8 |
9 | ```bash
10 | # Install dependancies
11 | $ npm install
12 | # Start the application
13 | $ npm start
14 | ```
15 |
16 | You can then access the app on : [http://localhost:3000/](http://localhost:3000/)
--------------------------------------------------------------------------------
/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/anancarv/fullstackopen/3b12bbb3f9ca4ca8c5b9c29df558d787e1009e8c/part6/unicafe-redux/public/favicon.ico
--------------------------------------------------------------------------------
/part6/unicafe-redux/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anancarv/fullstackopen/3b12bbb3f9ca4ca8c5b9c29df558d787e1009e8c/part6/unicafe-redux/public/logo192.png
--------------------------------------------------------------------------------
/part6/unicafe-redux/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anancarv/fullstackopen/3b12bbb3f9ca4ca8c5b9c29df558d787e1009e8c/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 counterReducer from './reducers/counterReducer'
5 |
6 | const store = createStore(counterReducer)
7 |
8 | const App = () => {
9 | return (
10 |
11 |
store.dispatch({type: 'GOOD'})}>good
12 |
store.dispatch({type: 'OK'})}>neutral
13 |
store.dispatch({type: 'BAD'})}>bad
14 |
store.dispatch({type: 'ZERO'})}>reset stats
15 |
good {store.getState().good}
16 |
neutral {store.getState().ok}
17 |
bad {store.getState().bad}
18 |
19 | )
20 | }
21 |
22 | const renderApp = () => {
23 | ReactDOM.render( , document.getElementById('root'))
24 | }
25 |
26 | renderApp()
27 | store.subscribe(renderApp)
28 |
--------------------------------------------------------------------------------
/part6/unicafe-redux/src/reducers/counterReducer.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 'DO_NOTHING':
11 | return state
12 | case 'GOOD':
13 | return {
14 | ...state,
15 | good: state.good + 1
16 | }
17 | case 'OK':
18 | return {
19 | ...state,
20 | ok: state.ok + 1
21 | }
22 | case 'BAD':
23 | return {
24 | ...state,
25 | bad: state.bad + 1
26 | }
27 | case 'ZERO':
28 | return initialState
29 | default: return state
30 | }
31 |
32 | }
33 |
34 | export default counterReducer
--------------------------------------------------------------------------------
/part7/README.md:
--------------------------------------------------------------------------------
1 | # Solutions for part 7 exercises
2 |
3 | The seventh part of the course touches on several different themes. First, we'll get familiar with React router. React router helps us divide the application into different views that are shown based on the URL in the browser's address bar. After this, we'll look at a few more ways to add CSS-styles to React applications. During the entire course we've used create-react-app to generate the body of our applications. This time we'll take a look under the hood: we'll learn how Webpack works and how we can use it to configure the application ourselves. We shall also have a look on hook-functions and how to define a custom hook.
--------------------------------------------------------------------------------
/part7/bloglist/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig helps developers define and maintain consistent
2 | # coding styles between different editors and IDEs
3 | # editorconfig.org
4 |
5 | root = true
6 |
7 | [*]
8 |
9 | # Change these settings to your own preference
10 | indent_style = space
11 | indent_size = 2
12 |
13 | # We recommend you to keep these unchanged
14 | end_of_line = lf
15 | charset = utf-8
16 | trim_trailing_whitespace = true
17 | insert_final_newline = true
18 |
19 | [*.md]
20 | trim_trailing_whitespace = false
--------------------------------------------------------------------------------
/part7/bloglist/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | build
3 | cypress
4 |
--------------------------------------------------------------------------------
/part7/bloglist/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "env": {
3 | "browser": true,
4 | "es6": true,
5 | "jest/globals": true
6 | },
7 | "extends": [
8 | "eslint:recommended",
9 | "plugin:react/recommended"
10 | ],
11 | "parserOptions": {
12 | "ecmaFeatures": {
13 | "jsx": true
14 | },
15 | "ecmaVersion": 2018,
16 | "sourceType": "module"
17 | },
18 | "plugins": [
19 | "react", "jest"
20 | ],
21 | "rules": {
22 | "indent": [
23 | "error",
24 | 2
25 | ],
26 | "quotes": [
27 | "error",
28 | "single"
29 | ],
30 | "semi": [
31 | "error",
32 | "never"
33 | ],
34 | "eqeqeq": "error",
35 | "no-trailing-spaces": "error",
36 | "object-curly-spacing": [
37 | "error", "always"
38 | ],
39 | "arrow-spacing": [
40 | "error", { "before": true, "after": true }
41 | ],
42 | "no-console": 0,
43 | "react/prop-types": 0
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/part7/bloglist/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/part7/bloglist/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | trailingComma: "none",
3 | tabWidth: 2,
4 | semi: false,
5 | singleQuote: true,
6 | };
7 |
--------------------------------------------------------------------------------
/part7/bloglist/README.md:
--------------------------------------------------------------------------------
1 | # Bloglist frontend
2 |
3 | In this exercise, we will refactor the Bloglist application that we worked on in parts four and five for it to use Redux for the application's state management. We also used `React Router` for conditional rendering of components based on the url in the browser, as well as `React Bootstrap` for styling our application.
4 |
5 | We assume that a user already exist in the database with the good credentials. If not, please head to part 4 for creating a new user using the API.
6 |
7 | ## Start the application locally
8 |
9 | To start an application:
10 |
11 | ```bash
12 | # First, you need to start the backend, to do so, head to the part4. Everything is explained in the README
13 |
14 | # Install dependancies
15 | $ npm install
16 |
17 | # Start the frontend application
18 | $ npm start
19 | ```
20 |
21 | You can then access the app on : http://localhost:3000/
--------------------------------------------------------------------------------
/part7/bloglist/cypress.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/part7/bloglist/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 | }
--------------------------------------------------------------------------------
/part7/bloglist/cypress/plugins/index.js:
--------------------------------------------------------------------------------
1 | ///
2 | // ***********************************************************
3 | // This example plugins/index.js can be used to load plugins
4 | //
5 | // You can change the location of this file or turn off loading
6 | // the plugins file with the 'pluginsFile' configuration option.
7 | //
8 | // You can read more here:
9 | // https://on.cypress.io/plugins-guide
10 | // ***********************************************************
11 |
12 | // This function is called when a project is opened or re-opened (e.g. due to
13 | // the project's config changing)
14 |
15 | /**
16 | * @type {Cypress.PluginConfig}
17 | */
18 | module.exports = (on, config) => {
19 | // `on` is used to hook into various events Cypress emits
20 | // `config` is the resolved Cypress config
21 | }
22 |
--------------------------------------------------------------------------------
/part7/bloglist/cypress/support/commands.js:
--------------------------------------------------------------------------------
1 | const storageKey = 'loggedBlogappUser'
2 |
3 | Cypress.Commands.add('login', ({ username, password }) => {
4 | cy.request('POST', 'http://localhost:3001/api/login', {
5 | username, password
6 | }).then(({ body }) => {
7 | localStorage.setItem(storageKey, JSON.stringify(body))
8 | cy.visit('http://localhost:3000')
9 | })
10 | })
11 |
12 | Cypress.Commands.add('createBlog', ({ title, author, url }) => {
13 | cy.request({
14 | url: 'http://localhost:3001/api/blogs',
15 | method: 'POST',
16 | body: { title, author, url },
17 | headers: {
18 | 'Authorization': `bearer ${JSON.parse(localStorage.getItem(storageKey)).token}`
19 | }
20 | })
21 |
22 | cy.visit('http://localhost:3000')
23 | })
24 |
--------------------------------------------------------------------------------
/part7/bloglist/cypress/support/index.js:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example support/index.js is processed and
3 | // loaded automatically before your test files.
4 | //
5 | // This is a great place to put global configuration and
6 | // behavior that modifies Cypress.
7 | //
8 | // You can change the location of this file or turn off
9 | // automatically serving support files with the
10 | // 'supportFile' configuration option.
11 | //
12 | // You can read more here:
13 | // https://on.cypress.io/configuration
14 | // ***********************************************************
15 |
16 | // Import commands.js using ES2015 syntax:
17 | import './commands'
18 |
19 | // Alternatively you can use CommonJS syntax:
20 | // require('./commands')
21 |
--------------------------------------------------------------------------------
/part7/bloglist/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-bootstrap": "^1.0.1",
11 | "react-dom": "^16.12.0",
12 | "react-redux": "^7.2.0",
13 | "react-router-dom": "^5.1.2",
14 | "react-scripts": "3.3.1",
15 | "redux": "^4.0.5",
16 | "redux-devtools-extension": "^2.13.8",
17 | "redux-thunk": "^2.3.0"
18 | },
19 | "scripts": {
20 | "start": "react-scripts start",
21 | "build": "react-scripts build",
22 | "test": "react-scripts test",
23 | "eject": "react-scripts eject",
24 | "lint": "eslint .",
25 | "cypress:open": "cypress open",
26 | "format": "prettier --write 'src/**/*.js'"
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.0",
48 | "eslint-plugin-jest": "^23.8.2",
49 | "prettier": "2.0.4"
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/part7/bloglist/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anancarv/fullstackopen/3b12bbb3f9ca4ca8c5b9c29df558d787e1009e8c/part7/bloglist/public/favicon.ico
--------------------------------------------------------------------------------
/part7/bloglist/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anancarv/fullstackopen/3b12bbb3f9ca4ca8c5b9c29df558d787e1009e8c/part7/bloglist/public/logo192.png
--------------------------------------------------------------------------------
/part7/bloglist/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anancarv/fullstackopen/3b12bbb3f9ca4ca8c5b9c29df558d787e1009e8c/part7/bloglist/public/logo512.png
--------------------------------------------------------------------------------
/part7/bloglist/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/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/part7/bloglist/src/components/Blog.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import '@testing-library/jest-dom/extend-expect'
3 | import { render, fireEvent } from '@testing-library/react'
4 | import Blog from './Blog'
5 |
6 | describe('Blog component tests', () => {
7 | let blog = {
8 | title: 'React patterns',
9 | author: 'Michael Chan',
10 | url: 'https://reactpatterns.com/',
11 | likes: 7
12 | }
13 |
14 | let mockUpdateBlog = jest.fn()
15 | let mockDeleteBlog = jest.fn()
16 |
17 | test('renders title and author', () => {
18 | const component = render(
19 |
24 | )
25 | expect(component.container).toHaveTextContent(
26 | 'React patterns - Michael Chan'
27 | )
28 | })
29 |
30 | test('clicking the view button displays url and number of likes', () => {
31 | const component = render(
32 |
37 | )
38 |
39 | const button = component.getByText('view')
40 | fireEvent.click(button)
41 |
42 | expect(component.container).toHaveTextContent('https://reactpatterns.com/')
43 |
44 | expect(component.container).toHaveTextContent('7')
45 | })
46 | })
47 |
--------------------------------------------------------------------------------
/part7/bloglist/src/components/BlogForm.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 BlogForm from './BlogForm'
5 |
6 | test(' updates parent state and calls onSubmit', () => {
7 | const createBlog = jest.fn()
8 |
9 | const component = render( )
10 |
11 | const input = component.container.querySelector('#title')
12 | const form = component.container.querySelector('form')
13 |
14 | fireEvent.change(input, {
15 | target: { value: 'Go To Statement Considered Harmful' }
16 | })
17 | fireEvent.submit(form)
18 |
19 | expect(createBlog.mock.calls).toHaveLength(1)
20 | expect(createBlog.mock.calls[0][0].title).toBe(
21 | 'Go To Statement Considered Harmful'
22 | )
23 | })
24 |
--------------------------------------------------------------------------------
/part7/bloglist/src/components/BlogList.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Blog from './Blog'
3 | import { useSelector } from 'react-redux'
4 |
5 | const BlogList = () => {
6 | const blogs = useSelector((state) => state.blog)
7 | const byLikes = (b1, b2) => b2.likes - b1.likes
8 |
9 | return blogs.sort(byLikes).map((blog) => )
10 | }
11 |
12 | export default BlogList
13 |
--------------------------------------------------------------------------------
/part7/bloglist/src/components/LoginForm.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { useDispatch } from 'react-redux'
3 | import { useHistory } from 'react-router-dom'
4 | import { login } from '../reducers/authReducer'
5 | import { initializeBlogs } from '../reducers/blogReducer'
6 | import { Form, Button } from 'react-bootstrap'
7 |
8 | const LoginForm = () => {
9 | const dispatch = useDispatch()
10 | const history = useHistory()
11 |
12 | const handleLogin = async (event) => {
13 | event.preventDefault()
14 | const username = event.target.username.value
15 | const password = event.target.password.value
16 | event.target.username.value = ''
17 | event.target.password.value = ''
18 | dispatch(login(username, password))
19 | dispatch(initializeBlogs())
20 | history.push('/blogs')
21 | }
22 |
23 | return (
24 |
26 | username:
27 |
32 | password:
33 |
38 |
39 | login
40 |
41 |
42 |
43 | )
44 | }
45 |
46 | export default LoginForm
47 |
--------------------------------------------------------------------------------
/part7/bloglist/src/components/Notification.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { useSelector } from 'react-redux'
3 | import { Alert } from 'react-bootstrap'
4 |
5 | const Notification = () => {
6 | const notification = useSelector((state) => state.notification)
7 |
8 | if (notification === null) {
9 | return null
10 | }
11 |
12 | if (notification.type === 'success') {
13 | return (
14 |
15 |
16 | {notification.message}
17 |
18 |
)
19 | } else {
20 | return (
21 |
22 |
23 | {notification.message}
24 |
25 |
)
26 | }
27 | }
28 |
29 | export default Notification
30 |
--------------------------------------------------------------------------------
/part7/bloglist/src/components/Togglable.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useImperativeHandle } from 'react'
2 | import { Button } from 'react-bootstrap'
3 | import PropTypes from 'prop-types'
4 |
5 | const Togglable = React.forwardRef((props, ref) => {
6 | const [visible, setVisible] = useState(false)
7 |
8 | const hideWhenVisible = { display: visible ? 'none' : '', paddingBottom: 5 }
9 | const showWhenVisible = { display: visible ? '' : 'none', paddingBottom: 5 }
10 |
11 | const toggleVisibility = () => {
12 | setVisible(!visible)
13 | }
14 |
15 | useImperativeHandle(ref, () => {
16 | return {
17 | toggleVisibility
18 | }
19 | })
20 |
21 | return (
22 |
23 |
24 | {props.buttonLabel}
25 |
26 |
27 | {props.children}
28 | cancel
29 |
30 |
31 | )
32 | })
33 |
34 | Togglable.displayName = 'Togglable'
35 | Togglable.propTypes = {
36 | buttonLabel: PropTypes.string.isRequired
37 | }
38 |
39 | export default Togglable
40 |
--------------------------------------------------------------------------------
/part7/bloglist/src/components/UserList.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { useSelector } from 'react-redux'
3 | import { Link } from 'react-router-dom'
4 |
5 | const User = ({ user }) => {
6 | return (
7 |
8 | {user.name} has {user.blogs.length} blogs
9 |
10 | )
11 | }
12 |
13 | const UserList = () => {
14 | const users = useSelector((state) => state.users)
15 | return users.map((user) => )
16 | }
17 |
18 | export default UserList
19 |
--------------------------------------------------------------------------------
/part7/bloglist/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 './utils/store'
6 | import App from './App'
7 |
8 | ReactDOM.render(
9 |
10 |
11 |
12 |
13 | ,
14 | document.getElementById('root')
15 | )
16 |
--------------------------------------------------------------------------------
/part7/bloglist/src/reducers/notificationReducer.js:
--------------------------------------------------------------------------------
1 | const notificationReducer = (state = null, action) => {
2 | switch (action.type) {
3 | case 'NEW_NOTIFICATION':
4 | return action.data
5 | case 'HIDE_NOTIFICATION':
6 | return action.data
7 | default:
8 | return state
9 | }
10 | }
11 |
12 | export const setNotification = (
13 | notification,
14 | notificationType,
15 | displayTime
16 | ) => {
17 | return async (dispatch) => {
18 | dispatch({
19 | type: 'NEW_NOTIFICATION',
20 | data: {
21 | message: notification,
22 | type: notificationType
23 | }
24 | })
25 |
26 | setTimeout(() => {
27 | dispatch({
28 | type: 'HIDE_NOTIFICATION',
29 | data: null
30 | })
31 | }, displayTime * 1000)
32 | }
33 | }
34 |
35 | export default notificationReducer
36 |
--------------------------------------------------------------------------------
/part7/bloglist/src/reducers/userReducer.js:
--------------------------------------------------------------------------------
1 | import userService from '../services/users'
2 |
3 | const userReducer = (state = [], action) => {
4 | switch (action.type) {
5 | case 'INIT_ALL_USERS':
6 | return action.data
7 | default:
8 | return state
9 | }
10 | }
11 |
12 | export const initializeAllUsers = () => {
13 | return async (dispatch) => {
14 | const users = await userService.getAll()
15 | dispatch({
16 | type: 'INIT_ALL_USERS',
17 | data: users
18 | })
19 | }
20 | }
21 |
22 | export default userReducer
23 |
--------------------------------------------------------------------------------
/part7/bloglist/src/services/blogs.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | const baseUrl = '/api/blogs'
3 |
4 | let token = null
5 | let config
6 |
7 | const setToken = (newToken) => {
8 | token = `bearer ${newToken}`
9 | config = {
10 | headers: { Authorization: token }
11 | }
12 | }
13 |
14 | const getAll = async () => {
15 | const response = await axios.get(baseUrl, config)
16 | return response.data
17 | }
18 |
19 | const create = async (newObject) => {
20 | const response = await axios.post(baseUrl, newObject, config)
21 | return response.data
22 | }
23 |
24 | const update = async (objectToUpdate) => {
25 | const response = await axios.put(
26 | `${baseUrl}/${objectToUpdate.id}`,
27 | objectToUpdate,
28 | config
29 | )
30 | return response.data
31 | }
32 |
33 | const remove = async (id) => {
34 | const response = await axios.delete(`${baseUrl}/${id}`, config)
35 | return response.data
36 | }
37 |
38 | export default { getAll, create, update, setToken, remove }
39 |
--------------------------------------------------------------------------------
/part7/bloglist/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/src/services/users.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | const baseUrl = '/api/users'
3 |
4 | const getAll = async () => {
5 | const response = await axios.get(baseUrl)
6 | return response.data
7 | }
8 |
9 | export default { getAll }
10 |
--------------------------------------------------------------------------------
/part7/bloglist/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 |
--------------------------------------------------------------------------------
/part7/bloglist/src/utils/store.js:
--------------------------------------------------------------------------------
1 | import { createStore, combineReducers, applyMiddleware } from 'redux'
2 | import thunk from 'redux-thunk'
3 | import { composeWithDevTools } from 'redux-devtools-extension'
4 |
5 | import userReducer from '../reducers/userReducer'
6 | import authReducer from '../reducers/authReducer'
7 | import blogReducer from '../reducers/blogReducer'
8 | import notificationReducer from '../reducers/notificationReducer'
9 |
10 | const reducer = combineReducers({
11 | user: authReducer,
12 | users: userReducer,
13 | blog: blogReducer,
14 | notification: notificationReducer
15 | })
16 |
17 | const store = createStore(reducer, composeWithDevTools(applyMiddleware(thunk)))
18 |
19 | export default store
20 |
--------------------------------------------------------------------------------
/part7/country-hook/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/part7/country-hook/README.md:
--------------------------------------------------------------------------------
1 | # Countries exercises with custom hooks
2 |
3 | In this exercise, we made a new version of the countries exercise from part 2 using custom hooks.
4 |
5 | This application is used to search for country details from the https://restcountries.eu/ interface. If country is found, the details of the country are displayed.
6 |
7 | ## Start the application
8 |
9 | To start an application, do the following :
10 |
11 | ```bash
12 | # Install dependancies
13 | $ npm install
14 |
15 | # Start the application
16 | $ npm start
17 | ```
18 |
19 | You can then access the app on : [http://localhost:3000/](http://localhost:3000/)
20 |
--------------------------------------------------------------------------------
/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/anancarv/fullstackopen/3b12bbb3f9ca4ca8c5b9c29df558d787e1009e8c/part7/country-hook/public/favicon.ico
--------------------------------------------------------------------------------
/part7/country-hook/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anancarv/fullstackopen/3b12bbb3f9ca4ca8c5b9c29df558d787e1009e8c/part7/country-hook/public/logo192.png
--------------------------------------------------------------------------------
/part7/country-hook/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anancarv/fullstackopen/3b12bbb3f9ca4ca8c5b9c29df558d787e1009e8c/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/App.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import { useField, useCountry } from './hooks'
3 | import Country from './components/Country'
4 |
5 | const App = () => {
6 | const nameInput = useField('text')
7 | const [name, setName] = useState('')
8 | const country = useCountry(name)
9 |
10 | const fetch = (event) => {
11 | event.preventDefault()
12 | setName(nameInput.value)
13 | }
14 |
15 | return (
16 |
17 |
21 |
22 |
23 |
24 | )
25 | }
26 |
27 | export default App
--------------------------------------------------------------------------------
/part7/country-hook/src/components/Country.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const Country = ({ country }) => {
4 | if (!country) {
5 | return null
6 | }
7 |
8 | if (country.length === 0) {
9 | return (
10 |
11 | not found...
12 |
13 | )
14 | }
15 |
16 | const countryObject = country[0]
17 |
18 | return (
19 |
20 |
{countryObject.name}
21 |
capital {countryObject.capital}
22 |
population {countryObject.population}
23 |
24 |
25 | )
26 | }
27 |
28 | export default Country
--------------------------------------------------------------------------------
/part7/country-hook/src/hooks/index.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react'
2 | import axios from 'axios'
3 |
4 | export const useField = (type) => {
5 | const [value, setValue] = useState('')
6 |
7 | const onChange = (event) => {
8 | setValue(event.target.value)
9 | }
10 |
11 | return {
12 | type,
13 | value,
14 | onChange
15 | }
16 | }
17 |
18 | export const useCountry = (name) => {
19 | const [country, setCountry] = useState(null)
20 |
21 | useEffect(() => {
22 | axios
23 | .get(`https://restcountries.eu/rest/v2/name/${name}?fullText=true`)
24 | .then(response => {
25 | console.log(response)
26 | setCountry(response.data)
27 | })
28 | }, [name])
29 |
30 | if ( name === '') {
31 | return null
32 | }
33 |
34 | if (!country) {
35 | return []
36 | }
37 |
38 | return country
39 | }
--------------------------------------------------------------------------------
/part7/country-hook/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import App from './App'
4 |
5 | ReactDOM.render( , document.getElementById('root'))
--------------------------------------------------------------------------------
/part7/routed-anecdotes/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | build
--------------------------------------------------------------------------------
/part7/routed-anecdotes/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "env": {
3 | "browser": true,
4 | "es6": true,
5 | "jest/globals": true
6 | },
7 | "extends": [
8 | "eslint:recommended",
9 | "plugin:react/recommended"
10 | ],
11 | "parserOptions": {
12 | "ecmaFeatures": {
13 | "jsx": true
14 | },
15 | "ecmaVersion": 2018,
16 | "sourceType": "module"
17 | },
18 | "plugins": [
19 | "react", "jest"
20 | ],
21 | "rules": {
22 | "indent": [
23 | "error",
24 | 2
25 | ],
26 | "quotes": [
27 | "error",
28 | "single"
29 | ],
30 | "semi": [
31 | "error",
32 | "never"
33 | ],
34 | "eqeqeq": "error",
35 | "no-trailing-spaces": "error",
36 | "object-curly-spacing": [
37 | "error", "always"
38 | ],
39 | "arrow-spacing": [
40 | "error", { "before": true, "after": true }
41 | ],
42 | "no-console": 0,
43 | "react/prop-types": 0
44 | }
45 | }
--------------------------------------------------------------------------------
/part7/routed-anecdotes/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/part7/routed-anecdotes/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | trailingComma: "none",
3 | tabWidth: 2,
4 | semi: false,
5 | singleQuote: true,
6 | };
--------------------------------------------------------------------------------
/part7/routed-anecdotes/README.md:
--------------------------------------------------------------------------------
1 | # Routed anecdotes
2 |
3 | In this exercise, we made a new version of the anecdote voting application from part 1 using React Router.
4 |
5 | Indeed, the app were not very optimal. The address always stayed the same even though at times we are in different views. Each view should preferably have its own address, e.g. to make bookmarking possible. If the application were to grow bigger and we wanted to, for example, add separate views for each user and anecdote, then the navigation management of the application, would get overly complicated.
6 |
7 | To fix this issue, we used the [React router](https://github.com/ReactTraining/react-router) library
8 |
9 | ## Start the application
10 |
11 | To start an application, do the following :
12 |
13 | ```bash
14 | # Install dependancies
15 | $ npm install
16 |
17 | # Start the application
18 | $ npm start
19 | ```
20 |
21 | You can then access the app on : [http://localhost:3000/](http://localhost:3000/)
--------------------------------------------------------------------------------
/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 | "prop-types": "^15.7.2",
10 | "react": "^16.12.0",
11 | "react-dom": "^16.12.0",
12 | "react-router-dom": "^5.1.2",
13 | "react-scripts": "3.3.1"
14 | },
15 | "scripts": {
16 | "start": "react-scripts start",
17 | "build": "react-scripts build",
18 | "test": "react-scripts test",
19 | "eject": "react-scripts eject",
20 | "lint": "eslint .",
21 | "format": "prettier --write 'src/**/*.js'"
22 | },
23 | "eslintConfig": {
24 | "extends": "react-app"
25 | },
26 | "browserslist": {
27 | "production": [
28 | ">0.2%",
29 | "not dead",
30 | "not op_mini all"
31 | ],
32 | "development": [
33 | "last 1 chrome version",
34 | "last 1 firefox version",
35 | "last 1 safari version"
36 | ]
37 | },
38 | "devDependencies": {
39 | "eslint-plugin-jest": "^23.8.2",
40 | "prettier": "2.0.5"
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/part7/routed-anecdotes/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anancarv/fullstackopen/3b12bbb3f9ca4ca8c5b9c29df558d787e1009e8c/part7/routed-anecdotes/public/favicon.ico
--------------------------------------------------------------------------------
/part7/routed-anecdotes/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anancarv/fullstackopen/3b12bbb3f9ca4ca8c5b9c29df558d787e1009e8c/part7/routed-anecdotes/public/logo192.png
--------------------------------------------------------------------------------
/part7/routed-anecdotes/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anancarv/fullstackopen/3b12bbb3f9ca4ca8c5b9c29df558d787e1009e8c/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/components/About.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const About = () => (
4 |
5 |
About anecdote app
6 |
According to Wikipedia:
7 |
8 |
9 | An anecdote is a brief, revealing account of an individual person or an
10 | incident. Occasionally humorous, anecdotes differ from jokes because their
11 | primary purpose is not simply to provoke laughter but to reveal a truth
12 | more general than the brief tale itself, such as to characterize a person
13 | by delineating a specific quirk or trait, to communicate an abstract idea
14 | about a person, place, or thing through the concrete details of a short
15 | narrative. An anecdote is "a story with a point."
16 |
17 |
18 |
19 | Software engineering is full of excellent anecdotes, at this app you can
20 | find the best and add more.
21 |
22 |
23 | )
24 |
25 | export default About
26 |
--------------------------------------------------------------------------------
/part7/routed-anecdotes/src/components/Anecdote.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | const Anecdote = ({ anecdote }) => {
5 | return (
6 |
7 |
{anecdote.content}
8 |
9 |
Author: {anecdote.author}
10 |
Has {anecdote.votes} votes
11 |
12 | For more info see: {anecdote.info}
13 |
14 |
15 |
16 | )
17 | }
18 |
19 | Anecdote.propTypes = {
20 | anecdote: PropTypes.object
21 | }
22 |
23 | export default Anecdote
24 |
--------------------------------------------------------------------------------
/part7/routed-anecdotes/src/components/AnecdoteList.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import { Link } from 'react-router-dom'
4 |
5 | const AnecdoteList = ({ anecdotes }) => (
6 |
7 |
Anecdotes
8 |
9 | {anecdotes.map((anecdote) => (
10 |
11 | {anecdote.content}
12 |
13 | ))}
14 |
15 |
16 | )
17 |
18 | AnecdoteList.propTypes = {
19 | anecdotes: PropTypes.arrayOf(PropTypes.object).isRequired
20 | }
21 |
22 | export default AnecdoteList
23 |
--------------------------------------------------------------------------------
/part7/routed-anecdotes/src/components/Footer.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const Footer = () => (
4 |
15 | )
16 |
17 | export default Footer
18 |
--------------------------------------------------------------------------------
/part7/routed-anecdotes/src/components/Menu.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Link } from 'react-router-dom'
3 |
4 | const Menu = () => {
5 | const padding = {
6 | paddingRight: 5
7 | }
8 | return (
9 |
10 |
11 | anecdotes
12 |
13 |
14 | create new
15 |
16 |
17 | about
18 |
19 |
20 | )
21 | }
22 |
23 | export default Menu
24 |
--------------------------------------------------------------------------------
/part7/routed-anecdotes/src/components/Notification.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | const Notification = ({ notification }) => {
5 | const style = {
6 | border: 'solid',
7 | padding: 10,
8 | borderWidth: 1
9 | }
10 |
11 | if (notification === null) {
12 | return null
13 | }
14 |
15 | return {notification}
16 | }
17 |
18 | Notification.propTypes = {
19 | notification: PropTypes.string
20 | }
21 |
22 | export default Notification
23 |
--------------------------------------------------------------------------------
/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 = () => {
11 | setValue('')
12 | }
13 |
14 | return {
15 | type,
16 | value,
17 | reset,
18 | onChange
19 | }
20 | }
--------------------------------------------------------------------------------
/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/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/part7/ultimate-hooks/README.md:
--------------------------------------------------------------------------------
1 | # Ultimate hooks
2 |
3 | In this exercise, we are refactoring [this app](https://github.com/fullstack-hy2020/ultimate-hooks) using custom hooks. This app displays notes and phone numbers fetched from a backend server.
4 |
5 | However, we noticed that the same code responsible for fetching notes from the backend could be reused in the blog post application. Indeed, only the `baseUrl` differs. As a result, we extracted the code for communicating with a backend server into its own `useResource` hook.
6 |
7 | ## Start the application
8 |
9 | To start an application, do the following :
10 |
11 | ```bash
12 | # Install dependancies
13 | $ npm install
14 | # Start the JSON Server
15 | $ npm run server
16 | # On another terminal, start the application
17 | $ npm start
18 | ```
19 |
20 | You can then access the app on : http://localhost:3000/
21 |
22 | You can also see the content of the JSON Server by heading to:
23 | * http://localhost:3005/notes
24 | * http://localhost:3005/persons
25 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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/anancarv/fullstackopen/3b12bbb3f9ca4ca8c5b9c29df558d787e1009e8c/part7/ultimate-hooks/public/favicon.ico
--------------------------------------------------------------------------------
/part7/ultimate-hooks/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anancarv/fullstackopen/3b12bbb3f9ca4ca8c5b9c29df558d787e1009e8c/part7/ultimate-hooks/public/logo192.png
--------------------------------------------------------------------------------
/part7/ultimate-hooks/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anancarv/fullstackopen/3b12bbb3f9ca4ca8c5b9c29df558d787e1009e8c/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/App.js:
--------------------------------------------------------------------------------
1 |
2 | import React from 'react'
3 | import { useField, useResource } from './hooks'
4 |
5 | const App = () => {
6 | const content = useField('text')
7 | const name = useField('text')
8 | const number = useField('text')
9 |
10 | const [notes, noteService] = useResource('http://localhost:3005/notes')
11 | const [persons, personService] = useResource('http://localhost:3005/persons')
12 |
13 | const handleNoteSubmit = (event) => {
14 | event.preventDefault()
15 | noteService.create({ content: content.value })
16 | }
17 |
18 | const handlePersonSubmit = (event) => {
19 | event.preventDefault()
20 | personService.create({ name: name.value, number: number.value})
21 | }
22 |
23 | return (
24 |
25 |
notes
26 |
30 | {notes.map(n =>
{n.content}
)}
31 |
32 |
persons
33 |
38 | {persons.map(n =>
{n.name} {n.number}
)}
39 |
40 | )
41 | }
42 |
43 | export default App
--------------------------------------------------------------------------------
/part7/ultimate-hooks/src/hooks/index.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react'
2 | import axios from 'axios'
3 |
4 | export const useField = (type) => {
5 | const [value, setValue] = useState('')
6 |
7 | const onChange = (event) => {
8 | setValue(event.target.value)
9 | }
10 |
11 | return {
12 | type,
13 | value,
14 | onChange
15 | }
16 | }
17 |
18 | export const useResource = (baseUrl) => {
19 | const [resources, setResources] = useState([])
20 |
21 | useEffect(() => {
22 | axios
23 | .get(baseUrl)
24 | .then(response => {
25 | setResources(response.data)
26 | })
27 | }, [setResources, baseUrl])
28 |
29 | const create = async newObject => {
30 | const response = await axios.post(baseUrl, newObject)
31 | setResources(resources.concat(response.data))
32 | }
33 |
34 | const service = {
35 | create
36 | }
37 |
38 | return [
39 | resources, service
40 | ]
41 | }
--------------------------------------------------------------------------------
/part7/ultimate-hooks/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import App from './App'
4 |
5 | ReactDOM.render( , document.getElementById('root'))
6 |
--------------------------------------------------------------------------------
/part8/README.md:
--------------------------------------------------------------------------------
1 | # Solutions for part 8 exercises
2 |
3 | This part of the course is about GraphQL, Facebook's alternative to REST for communication between browser and a server.
4 |
--------------------------------------------------------------------------------
/part8/library-backend/README.md:
--------------------------------------------------------------------------------
1 | ## GraphQL backend
2 |
3 | Through the exercises, we will implement a GraphQL backend for a small library.
4 |
5 |
6 | ### Start the application locally
7 | First create a `.env` file with the following content:
8 | ```
9 | MONGO_PWD=
10 | JWT_SECRET=
11 | PASSWORD= # All users have the same password, we focus here on GraphQL
12 | ```
13 |
14 | To start an application:
15 | ```bash
16 | # Install dependancies
17 | $ npm install
18 |
19 | # Start the application
20 | $ npm run dev
21 | ```
22 |
23 | You can then access the GraphQL-playground on: http://localhost:4000/.
24 |
25 | This is a very useful tool for a developer, and can be used to make queries to the server.
--------------------------------------------------------------------------------
/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 | })
14 |
15 | module.exports = mongoose.model('Author', schema)
--------------------------------------------------------------------------------
/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: [
18 | { type: String}
19 | ]
20 | })
21 |
22 | module.exports = mongoose.model('Book', schema)
--------------------------------------------------------------------------------
/part8/library-backend/models/users.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose')
2 | const uniqueValidator = require('mongoose-unique-validator')
3 |
4 | const schema = new mongoose.Schema({
5 | username: {
6 | type: String,
7 | required: true,
8 | unique: true,
9 | minlength: 3
10 | },
11 | favoriteGenre: {
12 | type: String,
13 | required: true,
14 | },
15 | })
16 |
17 | schema.plugin(uniqueValidator)
18 | module.exports = mongoose.model('User', schema)
--------------------------------------------------------------------------------
/part8/library-backend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "library-backend",
3 | "version": "1.0.0",
4 | "description": "GraphQL backend for a small library",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "node index.js",
8 | "dev": "nodemon index.js",
9 | "test": "echo \"Error: no test specified\" && exit 1"
10 | },
11 | "author": "",
12 | "license": "ISC",
13 | "devDependencies": {
14 | "nodemon": "^2.0.7"
15 | },
16 | "dependencies": {
17 | "apollo-server": "^2.21.2",
18 | "dotenv": "^9.0.2",
19 | "graphql": "^15.5.0",
20 | "jsonwebtoken": "^8.5.1",
21 | "mongoose": "^5.12.8",
22 | "mongoose-unique-validator": "^2.0.3",
23 | "uuid": "^8.3.2"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/part8/library-frontend/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/part8/library-frontend/README.md:
--------------------------------------------------------------------------------
1 | ## GraphQL frontend
2 |
3 | Through the exercises, we will implement a GraphQL frontend for the GraphQL-library created previously.
4 |
5 |
6 | ### Start the application locally
7 | First, you need to start the backend from the previous exercise. To do so, head to the `part8/library-backend` directory and follow the instructions from the README.
8 |
9 |
10 | Then, start the frontend:
11 | ```bash
12 | # Install dependancies
13 | $ npm install
14 |
15 | # Start the application
16 | $ npm start
17 | ```
18 |
19 | You can then access the app on: http://localhost:3000/.
--------------------------------------------------------------------------------
/part8/library-frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "frontend",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@apollo/client": "^3.3.15",
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.5.0",
12 | "react": "^16.12.0",
13 | "react-dom": "^16.12.0",
14 | "react-scripts": "3.4.0",
15 | "react-select": "^4.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 | },
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 | }
39 |
--------------------------------------------------------------------------------
/part8/library-frontend/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anancarv/fullstackopen/3b12bbb3f9ca4ca8c5b9c29df558d787e1009e8c/part8/library-frontend/public/favicon.ico
--------------------------------------------------------------------------------
/part8/library-frontend/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anancarv/fullstackopen/3b12bbb3f9ca4ca8c5b9c29df558d787e1009e8c/part8/library-frontend/public/logo192.png
--------------------------------------------------------------------------------
/part8/library-frontend/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anancarv/fullstackopen/3b12bbb3f9ca4ca8c5b9c29df558d787e1009e8c/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 from 'react'
2 | import { useQuery } from '@apollo/client'
3 | import BornYearForm from './BornYearForm'
4 | import { ALL_AUTHORS } from '../queries'
5 |
6 | const Authors = ({show, notify}) => {
7 | const result = useQuery(ALL_AUTHORS)
8 |
9 | if (!show) {
10 | return null
11 | }
12 |
13 | if (result.loading) {
14 | return loading...
15 | }
16 |
17 | return (
18 |
19 |
authors
20 |
21 |
22 |
23 |
24 |
25 | born
26 |
27 |
28 | books
29 |
30 |
31 | {result.data.allAuthors.map(a =>
32 |
33 | {a.name}
34 | {a.born}
35 | {a.bookCount}
36 |
37 | )}
38 |
39 |
40 |
41 |
42 |
43 | )
44 | }
45 |
46 | export default Authors
47 |
--------------------------------------------------------------------------------
/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 }) => {
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]) // eslint-disable-line
22 |
23 | const submit = async (event) => {
24 | event.preventDefault()
25 |
26 | login({ variables: { username, password } })
27 | }
28 |
29 | return (
30 |
48 | )
49 | }
50 |
51 | export default LoginForm
--------------------------------------------------------------------------------
/part8/library-frontend/src/components/Recommended.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react'
2 | import { useLazyQuery, useQuery } from '@apollo/client'
3 | import { ME, ALL_BOOKS_WITH_GENRE } from '../queries'
4 |
5 | const Recommended = ({ show }) => {
6 | const user = useQuery(ME)
7 | const [getFavoriteBooks, result] = useLazyQuery(ALL_BOOKS_WITH_GENRE)
8 | const [favoriteBooks, setFavoriteBooks] = useState([])
9 |
10 | useEffect(() => {
11 | if (result.data) {
12 | setFavoriteBooks(result.data.allBooks)
13 | }
14 | }, [setFavoriteBooks, result])
15 |
16 | useEffect(() => {
17 | if (user.data) {
18 | getFavoriteBooks({ variables: { genre: user.data.me.favoriteGenre } })
19 | }
20 | }, [getFavoriteBooks, user])
21 |
22 | if (!show) {
23 | return null
24 | }
25 |
26 | return (
27 |
28 |
29 | books in your favorite genre {user.data.me.favoriteGenre}
30 |
31 |
32 |
33 |
34 |
35 | author
36 | published
37 |
38 | {favoriteBooks.map((a) => (
39 |
40 | {a.title}
41 | {a.author.name}
42 | {a.published}
43 |
44 | ))}
45 |
46 |
47 |
48 | )
49 | }
50 |
51 | export default Recommended
--------------------------------------------------------------------------------
/part8/library-frontend/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import App from './App'
4 |
5 | import { setContext } from 'apollo-link-context'
6 | import {
7 | ApolloClient, ApolloProvider, HttpLink, InMemoryCache
8 | } from '@apollo/client'
9 |
10 | const authLink = setContext((_, { headers }) => {
11 | const token = localStorage.getItem('library-user-token')
12 | return {
13 | headers: {
14 | ...headers,
15 | authorization: token ? `bearer ${token}` : null,
16 | }
17 | }
18 | })
19 |
20 | const httpLink = new HttpLink({ uri: 'http://localhost:4000' })
21 |
22 | const client = new ApolloClient({
23 | cache: new InMemoryCache(),
24 | link: authLink.concat(httpLink)
25 | })
26 |
27 | ReactDOM.render(
28 |
29 |
30 | ,
31 | document.getElementById('root')
32 | )
--------------------------------------------------------------------------------
/part8/library-frontend/src/queries.js:
--------------------------------------------------------------------------------
1 | import { gql } from '@apollo/client'
2 |
3 | export const ALL_AUTHORS = gql`
4 | query {
5 | allAuthors {
6 | name
7 | born
8 | bookCount
9 | }
10 | }
11 | `
12 |
13 | export const ALL_BOOKS = gql`
14 | query {
15 | allBooks {
16 | title
17 | published
18 | genres
19 | author {
20 | name
21 | }
22 | }
23 | }
24 | `
25 |
26 | export const ME = gql`
27 | query {
28 | me {
29 | username
30 | favoriteGenre
31 | }
32 | }
33 | `
34 |
35 | export const ALL_BOOKS_WITH_GENRE = gql`
36 | query getallBooks($genre: String!) {
37 | allBooks(genre: $genre) {
38 | title
39 | published
40 | genres
41 | author {
42 | name
43 | }
44 | }
45 | }
46 | `
47 |
48 | export const CREATE_BOOK = gql`
49 | mutation createBook($title: String!, $author: String!, $published: Int!, $genres: [String!]!){
50 | addBook(
51 | title: $title,
52 | author: $author,
53 | published: $published,
54 | genres: $genres
55 | ) {
56 | title,
57 | author
58 | }
59 | }
60 | `
61 |
62 | export const EDIT_BORN_YEAR = gql`
63 | mutation changeBornYear($name: String!, $setBornTo: Int!){
64 | editAuthor(
65 | name: $name,
66 | setBornTo: $setBornTo
67 | ) {
68 | name
69 | born
70 | }
71 | }
72 | `
73 |
74 | export const LOGIN = gql`
75 | mutation login($username: String!, $password: String!) {
76 | login(username: $username, password: $password) {
77 | value
78 | }
79 | }
80 | `
--------------------------------------------------------------------------------
/part9/README.md:
--------------------------------------------------------------------------------
1 | # Solutions for part 9 exercises
2 |
3 | This part is all about TypeScript: and open-source typed superset of JavaScript developed by Microsoft that compiles to plain JavaScript.
4 |
5 | In this part we will be using the tools previously introduced to build end-to-end features to an existing ecosystem with linters predefined and an existing codebase writing TypeScript.
--------------------------------------------------------------------------------
/part9/courseinfo/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es6": true,
5 | "jest": true
6 | },
7 | "extends": [
8 | "eslint:recommended",
9 | "plugin:react/recommended",
10 | "plugin:@typescript-eslint/recommended"
11 | ],
12 | "plugins": ["react", "@typescript-eslint"],
13 | "settings": {
14 | "react": {
15 | "pragma": "React",
16 | "version": "detect"
17 | }
18 | },
19 | "rules": {
20 | "@typescript-eslint/explicit-function-return-type": 0
21 | }
22 | }
--------------------------------------------------------------------------------
/part9/courseinfo/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/part9/courseinfo/README.md:
--------------------------------------------------------------------------------
1 | # Course information
2 |
3 | Simple web applicaton for understanding the core concepts of TypeScript with React
4 |
5 | ## Start the application
6 |
7 | To start an application, do the following :
8 |
9 | ```bash
10 | # Install dependancies
11 | $ yarn install
12 |
13 | # Start the application
14 | $ yarn start
15 | ```
16 |
17 | You can then access the app on : [http://localhost:3000/](http://localhost:3000/)
--------------------------------------------------------------------------------
/part9/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.3.2",
8 | "@testing-library/user-event": "^7.1.2",
9 | "@types/jest": "^24.0.0",
10 | "@types/node": "^12.0.0",
11 | "@types/react": "^16.9.0",
12 | "@types/react-dom": "^16.9.0",
13 | "react": "^16.13.1",
14 | "react-dom": "^16.13.1",
15 | "react-scripts": "3.4.1",
16 | "typescript": "~3.7.2"
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 './src/**/*.{ts,tsx}'"
24 | },
25 | "eslintConfig": {
26 | "extends": "react-app"
27 | },
28 | "browserslist": {
29 | "production": [
30 | ">0.2%",
31 | "not dead",
32 | "not op_mini all"
33 | ],
34 | "development": [
35 | "last 1 chrome version",
36 | "last 1 firefox version",
37 | "last 1 safari version"
38 | ]
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/part9/courseinfo/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anancarv/fullstackopen/3b12bbb3f9ca4ca8c5b9c29df558d787e1009e8c/part9/courseinfo/public/favicon.ico
--------------------------------------------------------------------------------
/part9/courseinfo/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anancarv/fullstackopen/3b12bbb3f9ca4ca8c5b9c29df558d787e1009e8c/part9/courseinfo/public/logo192.png
--------------------------------------------------------------------------------
/part9/courseinfo/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anancarv/fullstackopen/3b12bbb3f9ca4ca8c5b9c29df558d787e1009e8c/part9/courseinfo/public/logo512.png
--------------------------------------------------------------------------------
/part9/courseinfo/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/part9/courseinfo/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/part9/courseinfo/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Header from './components/Header'
3 | import Content from './components/Content'
4 | import Total from './components/Total'
5 | import { CoursePartOne, CoursePartTwo, CoursePartThree, CoursePartFour } from './types'
6 |
7 | type CoursePart = CoursePartOne | CoursePartTwo | CoursePartThree | CoursePartFour;
8 |
9 |
10 | const App: React.FC = () => {
11 | const courseName: string = "Half Stack application development";
12 | const courseParts: CoursePart[] = [
13 | {
14 | name: "Fundamentals",
15 | exerciseCount: 10,
16 | description: "This is an awesome course part"
17 | },
18 | {
19 | name: "Using props to pass data",
20 | exerciseCount: 7,
21 | groupProjectCount: 3
22 | },
23 | {
24 | name: "Deeper type usage",
25 | exerciseCount: 14,
26 | description: "Confusing description",
27 | exerciseSubmissionLink: "https://fake-exercise-submit.made-up-url.dev"
28 | },
29 | {
30 | name: "Another course part",
31 | exerciseCount: 8,
32 | description: "Confusing description",
33 | comment: "This is a comment"
34 | }
35 | ];
36 |
37 | return (
38 |
39 |
40 |
41 |
42 |
43 | );
44 | };
45 |
46 | export default App;
47 |
--------------------------------------------------------------------------------
/part9/courseinfo/src/components/Content.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Part from './Part'
3 | import { ContentProps } from '../types'
4 |
5 | const Content: React.FC = ({courseParts}) =>
6 |
7 | {courseParts.map((part, i) =>
8 |
9 | )}
10 |
11 |
12 | export default Content;
--------------------------------------------------------------------------------
/part9/courseinfo/src/components/Header.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { HeaderProps } from '../types'
3 |
4 | const Header: React.FC = ({courseName}) => {courseName}
5 |
6 | export default Header
--------------------------------------------------------------------------------
/part9/courseinfo/src/components/Part.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { PartProps } from '../types'
3 |
4 | const Part: React.FC = ({part}) => {
5 | switch (part.name) {
6 | case "Fundamentals":
7 | return (
8 |
9 | {part.name} {part.description} {part.exerciseCount}
10 |
11 | )
12 | case "Using props to pass data":
13 | return (
14 |
15 | {part.name} {part.description} {part.exerciseCount} {part.groupProjectCount}
16 |
17 | )
18 | case "Deeper type usage":
19 | return (
20 |
21 | {part.name} {part.description} {part.exerciseCount} {part.exerciseSubmissionLink}
22 |
23 | )
24 | case "Another course part":
25 | return (
26 |
27 | {part.name} {part.description} {part.exerciseCount} {part.comment}
28 |
29 | )
30 | default:
31 | return null;
32 | }
33 | }
34 |
35 | export default Part;
--------------------------------------------------------------------------------
/part9/courseinfo/src/components/Total.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { ContentProps } from '../types'
3 |
4 | const Total: React.FC = ({courseParts}) => {
5 | const total = courseParts.reduce((sum, part) => sum + part.exerciseCount, 0)
6 |
7 | return (
8 | Number of exercises {total}
9 | )
10 | }
11 |
12 | export default Total
--------------------------------------------------------------------------------
/part9/courseinfo/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"));
--------------------------------------------------------------------------------
/part9/courseinfo/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/part9/courseinfo/src/types.ts:
--------------------------------------------------------------------------------
1 | export interface CoursePartBase {
2 | name: string;
3 | exerciseCount: number;
4 | description?: string;
5 | }
6 |
7 | export interface CoursePartOne extends CoursePartBase {
8 | name: "Fundamentals";
9 | description?: string;
10 | }
11 |
12 | export interface CoursePartTwo extends CoursePartBase {
13 | name: "Using props to pass data";
14 | groupProjectCount: number;
15 | }
16 |
17 | export interface CoursePartThree extends CoursePartBase {
18 | name: "Deeper type usage";
19 | exerciseSubmissionLink: string;
20 | }
21 |
22 | export interface CoursePartFour extends CoursePartBase {
23 | name: "Another course part";
24 | comment: string;
25 | }
26 |
27 | export interface HeaderProps {
28 | courseName: string;
29 | }
30 |
31 | type CoursePart = CoursePartOne | CoursePartTwo | CoursePartThree | CoursePartFour;
32 |
33 | export interface ContentProps {
34 | courseParts: CoursePart[];
35 | }
36 |
37 | export interface PartProps {
38 | part: CoursePart;
39 | }
--------------------------------------------------------------------------------
/part9/courseinfo/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/.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": ["error", { "argsIgnorePattern": "^_" }],
17 | "no-case-declarations": 0
18 | },
19 | "parser": "@typescript-eslint/parser",
20 | "parserOptions": {
21 | "project": "./tsconfig.json"
22 | }
23 | }
--------------------------------------------------------------------------------
/part9/first-steps/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # misc
12 | .DS_Store
13 | .env.local
14 | .env.development.local
15 | .env.test.local
16 | .env.production.local
17 |
18 | npm-debug.log*
19 | yarn-debug.log*
20 | yarn-error.log*
--------------------------------------------------------------------------------
/part9/first-steps/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "none",
3 | "tabWidth": 2,
4 | "semi": true,
5 | "singleQuote": true
6 | }
--------------------------------------------------------------------------------
/part9/first-steps/README.md:
--------------------------------------------------------------------------------
1 | # First steps with typescript
2 |
3 | Simple express applicaton for understanding the core concepts of TypeScript
4 |
5 | ## Start the application
6 |
7 | To start an application, do the following :
8 |
9 | ```bash
10 | # npm dependancies
11 | $ yarn install
12 | # Start the application
13 | $ npm start
14 | ```
15 |
16 | You can then access the app on : [http://localhost:3003/](http://localhost:3003/)
17 |
18 | # Endpoints
19 |
20 | The following enpoinds are available:
21 | * `/bmi`: For calculating `the body mass index` based on given weight (in kilograms) and height (in centimeters). For example to get bmi for a person having height 180 and weight 72, the url is http://localhost:3003/bmi?height=180&weight=72. The response is a json of the form:
22 |
23 | ```json
24 | {
25 | "weight": 72,
26 | "height": 180,
27 | "bmi": "Normal (healthy weight)"
28 | }
29 | ```
30 |
31 | * `/exercises`: That calculates the average time of daily exercise hours and compares it to the target amount of daily hours. It can be used by doing a HTTP POST request to `/exercises` exercises with the input in the request body:
32 |
33 | ```json
34 | {
35 | "daily_exercises": [1, 0, 2, 0, 3, 0, 2.5],
36 | "target": 2.5
37 | }
38 | ```
39 |
40 | The response is a json of the form:
41 | ```json
42 | {
43 | "periodLength": 7,
44 | "trainingDays": 4,
45 | "success": false,
46 | "rating": 1,
47 | "ratingDescription": "bad",
48 | "target": 2.5,
49 | "average": 1.2142857142857142
50 | }
51 | ```
--------------------------------------------------------------------------------
/part9/first-steps/bmiCalculator.ts:
--------------------------------------------------------------------------------
1 | interface BmiValues {
2 | heightInCm: number;
3 | weightInKg: number;
4 | }
5 |
6 | export const parseBmiArguments = (
7 | height: number,
8 | weight: number
9 | ): BmiValues => {
10 | if (!isNaN(height) && !isNaN(weight)) {
11 | return {
12 | heightInCm: height,
13 | weightInKg: weight
14 | };
15 | } else {
16 | throw new Error('Provided values were not numbers!');
17 | }
18 | };
19 |
20 | export const calculateBmi = (
21 | heightInCm: number,
22 | weightInKg: number
23 | ): string => {
24 | const bmi = (weightInKg / heightInCm / heightInCm) * 10000;
25 |
26 | if (bmi < 15) {
27 | return 'Very severely underweight';
28 | } else if (bmi > 15 && bmi < 16) {
29 | return 'Severely underweight';
30 | } else if (bmi > 16 && bmi < 18.5) {
31 | return 'Underweight';
32 | } else if (bmi > 18.5 && bmi < 25) {
33 | return 'Normal (healthy weight)';
34 | } else if (bmi > 25 && bmi < 30) {
35 | return 'Overweight';
36 | } else if (bmi > 30 && bmi < 35) {
37 | return 'Obese Class I (Moderately obese)';
38 | } else if (bmi > 35 && bmi < 40) {
39 | return 'Obese Class II (Severely obese)';
40 | } else {
41 | return 'Obese Class III (Very severely obese) ';
42 | }
43 | };
44 |
--------------------------------------------------------------------------------
/part9/first-steps/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "first-steps",
3 | "version": "1.0.0",
4 | "description": "First steps with Typescript",
5 | "main": "index.js",
6 | "scripts": {
7 | "ts-node": "ts-node",
8 | "calculateBmi": "ts-node bmiCalculator.ts",
9 | "calculateExercises": "ts-node exerciseCalculator.ts",
10 | "start": "ts-node index.ts",
11 | "dev": "ts-node-dev index.ts",
12 | "lint": "eslint --ext .ts .",
13 | "format": "prettier --write *.ts"
14 | },
15 | "author": "",
16 | "license": "ISC",
17 | "devDependencies": {
18 | "@types/body-parser": "^1.19.0",
19 | "@types/express": "^4.17.6",
20 | "@types/node": "^14.0.1",
21 | "@typescript-eslint/eslint-plugin": "^2.33.0",
22 | "@typescript-eslint/parser": "^2.33.0",
23 | "body-parser": "^1.19.0",
24 | "eslint": "^7.0.0",
25 | "prettier": "2.0.5",
26 | "ts-node": "^8.10.1",
27 | "ts-node-dev": "^1.0.0-pre.44",
28 | "typescript": "^3.8.3"
29 | },
30 | "dependencies": {
31 | "express": "^4.17.1"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/part9/first-steps/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "noImplicitAny": true,
4 | "noImplicitReturns": true,
5 | "strictNullChecks": true,
6 | "strictPropertyInitialization": true,
7 | "strictBindCallApply": true,
8 | "noUnusedLocals": true,
9 | "noUnusedParameters": true,
10 | "noImplicitThis": true,
11 | "alwaysStrict": true,
12 | "esModuleInterop": true,
13 | "declaration": true,
14 | }
15 | }
--------------------------------------------------------------------------------
/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", { "argsIgnorePattern": "^_" }
17 | ],
18 | "@typescript-eslint/no-explicit-any": 1,
19 | "no-case-declarations": 0
20 | },
21 | "parser": "@typescript-eslint/parser",
22 | "parserOptions": {
23 | "project": "./tsconfig.json"
24 | }
25 | }
--------------------------------------------------------------------------------
/part9/patientor-backend/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "none",
3 | "tabWidth": 2,
4 | "semi": true,
5 | "singleQuote": true
6 | }
--------------------------------------------------------------------------------
/part9/patientor-backend/README.md:
--------------------------------------------------------------------------------
1 | # Patientor Backend
2 |
3 | For this set of exercises we will be developing a backend for an existing project called Patientor which is a simple medical record application for doctors who handle diagnoses and basic health information of their patients.
4 |
5 | The [frontend](https://github.com/fullstack-hy2020/patientor) has already been built by outsider experts and our task is to create a backend to support the existing code.
6 |
7 | ## Start the application locally
8 |
9 | To start an application:
10 |
11 | ```bash
12 | # Install dependancies
13 | $ npm install
14 |
15 | # Start the application in dev environment
16 | $ npm run dev
17 |
18 | # Start the application in prod environment
19 | $ npm run tsc # Create a production build
20 | $ npm start
21 |
22 | # To start the frontend patientor app
23 | # Open a new terminal and head to the patientor-frontend directory
24 | $ cd ../patientor-frontend
25 | $ npm install
26 | $ npm start
27 | ```
28 |
29 | Then the two following endpoints are accessible:
30 | * http://localhost:3001/api/patients (POST)
31 | * http://localhost:3001/api/diagnoses (GET)
32 |
33 | To create a now patient (POST), the payload should look like this:
34 | ```json
35 | {
36 | "name": "John McClane",
37 | "dateOfBirth": "1986-07-09",
38 | "ssn": "090786-122X",
39 | "gender": "male",
40 | "occupation": "New york city cop"
41 | }
42 | ```
--------------------------------------------------------------------------------
/part9/patientor-backend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "patientor-backend",
3 | "version": "1.0.0",
4 | "description": "Backend API for a medical record application",
5 | "main": "src/index.ts",
6 | "scripts": {
7 | "tsc": "tsc",
8 | "dev": "ts-node-dev src/index.ts",
9 | "lint": "eslint --ext .ts .",
10 | "format": "prettier --write 'src/**/*.ts'",
11 | "start": "node build/index.js"
12 | },
13 | "author": "",
14 | "license": "ISC",
15 | "devDependencies": {
16 | "@types/cors": "^2.8.6",
17 | "@types/express": "^4.17.6",
18 | "@types/uuid": "^7.0.3",
19 | "@typescript-eslint/eslint-plugin": "^2.33.0",
20 | "@typescript-eslint/parser": "^2.33.0",
21 | "eslint": "^7.0.0",
22 | "prettier": "2.0.5",
23 | "ts-node-dev": "^1.0.0-pre.44",
24 | "typescript": "^3.9.2"
25 | },
26 | "dependencies": {
27 | "cors": "^2.8.5",
28 | "express": "^4.17.1",
29 | "uuid": "^8.0.0"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/part9/patientor-backend/src/index.ts:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import cors from 'cors';
3 | import diagnoseRouter from './routes/diagnoses';
4 | import patientRouter from './routes/patients';
5 |
6 | const app = express();
7 |
8 | app.use(express.json());
9 | app.use(cors());
10 |
11 | const PORT = 3001;
12 |
13 | app.get('/api/ping', (_req, res) => {
14 | console.log('someone pinged here');
15 | res.send('pong');
16 | });
17 |
18 | app.use('/api/diagnoses', diagnoseRouter);
19 | app.use('/api/patients', patientRouter);
20 |
21 | app.listen(PORT, () => {
22 | console.log(`Server running on port ${PORT}`);
23 | });
24 |
--------------------------------------------------------------------------------
/part9/patientor-backend/src/routes/diagnoses.ts:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import diagnoseService from '../services/diagnoseService';
3 |
4 | const diagnoseRouter = express.Router();
5 |
6 | diagnoseRouter.get('/', (_req, res) => {
7 | res.send(diagnoseService.getAll());
8 | });
9 |
10 | export default diagnoseRouter;
11 |
--------------------------------------------------------------------------------
/part9/patientor-backend/src/routes/patients.ts:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import patientService from '../services/patientService';
3 | import toNewPatientEntry from '../utils';
4 |
5 | const patientRouter = express.Router();
6 |
7 | patientRouter.get('/', (_req, res) => {
8 | res.send(patientService.getAll());
9 | });
10 |
11 | patientRouter.get('/:patientId', (req, res) => {
12 | res.send(patientService.getOne(req.params.patientId));
13 | });
14 |
15 | patientRouter.post('/', (req, res) => {
16 | try {
17 | const newDiaryEntry = toNewPatientEntry(req.body);
18 | const addedEntry = patientService.addPatient(newDiaryEntry);
19 | res.json(addedEntry);
20 | } catch (e) {
21 | res.status(400).send({ error: e.message });
22 | }
23 | });
24 |
25 | export default patientRouter;
26 |
--------------------------------------------------------------------------------
/part9/patientor-backend/src/services/diagnoseService.ts:
--------------------------------------------------------------------------------
1 | import diagnoseEntries from '../../data/diagnoses';
2 | import { DiagnoseEntry } from '../types';
3 |
4 | const diagnoses: Array = diagnoseEntries;
5 |
6 | const getAll = (): Array => {
7 | return diagnoses;
8 | };
9 |
10 | export default {
11 | getAll
12 | };
13 |
--------------------------------------------------------------------------------
/part9/patientor-backend/src/services/patientService.ts:
--------------------------------------------------------------------------------
1 | import { v4 as uuidv4 } from 'uuid';
2 | import patientEntries from '../../data/patients';
3 | import {
4 | NonSensitivePatientEntry,
5 | PatientEntry,
6 | NewPatientEntry
7 | } from '../types';
8 |
9 | const patients: Array = patientEntries.map(
10 | ({ id, name, dateOfBirth, gender, occupation, entries }) => ({
11 | id,
12 | name,
13 | dateOfBirth,
14 | gender,
15 | occupation,
16 | entries
17 | })
18 | );
19 |
20 | const getAll = (): Array => {
21 | return patients;
22 | };
23 |
24 | const getOne = (id: string): NonSensitivePatientEntry | undefined => {
25 | return patients.find((patient) => patient.id === id);
26 | };
27 |
28 | const addPatient = (entry: NewPatientEntry): PatientEntry => {
29 | const newPatientEntry = {
30 | id: uuidv4(),
31 | ...entry
32 | };
33 | patients.push(newPatientEntry);
34 | return newPatientEntry;
35 | };
36 |
37 | export default {
38 | getAll,
39 | getOne,
40 | addPatient
41 | };
42 |
--------------------------------------------------------------------------------
/part9/patientor-backend/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES6",
4 | "outDir": "./build/",
5 | "module": "commonjs",
6 | "strict": true,
7 | "noUnusedLocals": true,
8 | "noUnusedParameters": true,
9 | "noImplicitReturns": true,
10 | "noFallthroughCasesInSwitch": true,
11 | "esModuleInterop": true,
12 | "resolveJsonModule": true
13 | }
14 | }
--------------------------------------------------------------------------------
/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/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "none",
3 | "tabWidth": 2,
4 | "semi": true,
5 | "singleQuote": true
6 | }
--------------------------------------------------------------------------------
/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.2",
7 | "formik": "^2.1.4",
8 | "react": "^16.13.1",
9 | "react-dom": "^16.13.1",
10 | "react-router-dom": "^5.2.0",
11 | "semantic-ui-css": "^2.4.1",
12 | "semantic-ui-react": "^0.88.2"
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.35",
19 | "@types/react-dom": "16.9.3",
20 | "@types/react-router-dom": "^5.1.5",
21 | "@typescript-eslint/eslint-plugin": "^2.33.0",
22 | "@typescript-eslint/parser": "^2.33.0",
23 | "eslint-config-react": "^1.1.7",
24 | "prettier": "2.0.5",
25 | "react-scripts": "3.3.0",
26 | "typescript": "^3.9.2"
27 | },
28 | "scripts": {
29 | "start": "react-scripts start",
30 | "build": "react-scripts build",
31 | "test": "react-scripts test",
32 | "eject": "react-scripts eject",
33 | "lint": "eslint './src/**/*.{ts,tsx}'",
34 | "format": "prettier --write './src/**/*.{ts,tsx}'",
35 | "lint:fix": "eslint './src/**/*.{ts,tsx}' --fix"
36 | },
37 | "browserslist": {
38 | "production": [
39 | ">0.2%",
40 | "not dead",
41 | "not op_mini all"
42 | ],
43 | "development": [
44 | "last 1 chrome version",
45 | "last 1 firefox version",
46 | "last 1 safari version"
47 | ]
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/part9/patientor-frontend/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anancarv/fullstackopen/3b12bbb3f9ca4ca8c5b9c29df558d787e1009e8c/part9/patientor-frontend/public/favicon.ico
--------------------------------------------------------------------------------
/part9/patientor-frontend/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "start_url": ".",
5 | "display": "standalone",
6 | "theme_color": "#000000",
7 | "background_color": "#ffffff"
8 | }
9 |
--------------------------------------------------------------------------------
/part9/patientor-frontend/src/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/HealthCheck.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Icon, Card } from 'semantic-ui-react';
3 | import { HealthCheckEntry } from '../types';
4 |
5 | const style = { margin: 10 };
6 |
7 | const HealthCheck: React.FC<{ entry: HealthCheckEntry }> = ({ entry }) => {
8 | let color: 'green' | 'yellow' | 'orange' | 'red';
9 |
10 | switch (entry.healthCheckRating) {
11 | case 0:
12 | color = 'green';
13 | break;
14 | case 1:
15 | color = 'yellow';
16 | break;
17 | case 2:
18 | color = 'orange';
19 | break;
20 | case 3:
21 | color = 'red';
22 | break;
23 | default:
24 | color = 'green';
25 | break;
26 | }
27 |
28 | return (
29 |
30 |
31 |
32 | {entry.date}
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | );
41 | };
42 |
43 | export default HealthCheck;
44 |
--------------------------------------------------------------------------------
/part9/patientor-frontend/src/PatientPage/Hospital.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Icon, Card } from 'semantic-ui-react';
3 | import { HospitalEntry } from '../types';
4 |
5 | const style = { margin: 10 };
6 |
7 | const Hospital: React.FC<{ entry: HospitalEntry }> = ({ entry }) => (
8 |
9 |
10 |
11 | {entry.date}
12 |
13 |
14 |
15 |
16 | );
17 |
18 | export default Hospital;
19 |
--------------------------------------------------------------------------------
/part9/patientor-frontend/src/PatientPage/OccupationalHealthcare.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Icon, Card } from 'semantic-ui-react';
3 | import { OccupationalHealthcareEntry } from '../types';
4 |
5 | const style = { margin: 10 };
6 |
7 | const OccupationalHealthcare: React.FC<{
8 | entry: OccupationalHealthcareEntry;
9 | }> = ({ entry }) => (
10 |
11 |
12 |
13 | {entry.date}
14 |
15 |
16 |
17 |
18 | );
19 |
20 | export default OccupationalHealthcare;
21 |
--------------------------------------------------------------------------------
/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: { [id: 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/types.ts:
--------------------------------------------------------------------------------
1 | export interface Diagnosis {
2 | code: string;
3 | name: string;
4 | latin?: string;
5 | }
6 |
7 | export enum Gender {
8 | Male = 'male',
9 | Female = 'female',
10 | Other = 'other'
11 | }
12 |
13 | interface BaseEntry {
14 | id: string;
15 | description: string;
16 | date: string;
17 | specialist: string;
18 | diagnosisCodes?: Array;
19 | }
20 |
21 | export enum HealthCheckRating {
22 | 'Healthy' = 0,
23 | 'LowRisk' = 1,
24 | 'HighRisk' = 2,
25 | 'CriticalRisk' = 3
26 | }
27 |
28 | export interface HealthCheckEntry extends BaseEntry {
29 | type: 'HealthCheck';
30 | healthCheckRating: HealthCheckRating;
31 | }
32 |
33 | export interface OccupationalHealthcareEntry extends BaseEntry {
34 | type: 'OccupationalHealthcare';
35 | employerName: string;
36 | sickLeave?: {
37 | startDate: string;
38 | endDate: string;
39 | };
40 | }
41 |
42 | export interface HospitalEntry extends BaseEntry {
43 | type: 'Hospital';
44 | discharge: {
45 | date: string;
46 | criteria: string;
47 | };
48 | }
49 |
50 | export type Entry =
51 | | HospitalEntry
52 | | OccupationalHealthcareEntry
53 | | HealthCheckEntry;
54 |
55 | export interface Patient {
56 | id: string;
57 | name: string;
58 | occupation: string;
59 | gender: Gender;
60 | ssn?: string;
61 | dateOfBirth?: string;
62 | entries: Entry[];
63 | }
64 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------