├── .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 |
21 | 22 | 23 | 24 |
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 | 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} 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 |
    5 |
    6 | name: 7 |
    8 |
    9 | number: 10 |
    11 |
    12 | 13 |
    14 |
    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 |
    35 |
    36 | Title: 37 |
    38 |
    39 | Author: 40 |
    41 |
    42 | Url: 43 |
    44 |
    45 | 46 |
    47 |
    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 |
    7 |
    8 | username props.setUsername(target.value)}/> 9 |
    10 |
    11 | password props.setPassword(target.value)}/> 12 |
    13 | 14 |
    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 | 24 |
    25 |
    26 | {props.children} 27 | 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 |
    19 | 20 | 21 |
    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 | 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 | 12 | 13 | 14 | 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 |
    25 | 26 | username: 27 | 32 | password: 33 | 38 | 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 | 25 |
    26 |
    27 | {props.children} 28 | 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 |
    18 | 19 | 20 |
    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 | {`flag 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 |
    5 | Anecdote app for{' '} 6 | 7 | Full Stack -websovelluskehitys 8 | 9 | . See{' '} 10 | 11 | https://github.com/fullstack-hy2019/routed-anecdotes/blob/master/src/App.js 12 | {' '} 13 | for the source code. 14 |
    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 |
    27 | 28 | 29 |
    30 | {notes.map(n =>

    {n.content}

    )} 31 | 32 |

    persons

    33 |
    34 | name
    35 | number 36 | 37 |
    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 | 27 | 30 | 31 | {result.data.allAuthors.map(a => 32 | 33 | 34 | 35 | 36 | 37 | )} 38 | 39 |
    25 | born 26 | 28 | books 29 |
    {a.name}{a.born}{a.bookCount}
    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 |
    31 |
    32 |
    33 | username setUsername(target.value)} 36 | /> 37 |
    38 |
    39 | password setPassword(target.value)} 43 | /> 44 |
    45 | 46 |
    47 |
    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 | 36 | 37 | 38 | {favoriteBooks.map((a) => ( 39 | 40 | 41 | 42 | 43 | 44 | ))} 45 | 46 |
    authorpublished
    {a.title}{a.author.name}{a.published}
    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 | --------------------------------------------------------------------------------