├── .gitignore ├── README.md ├── part_0 ├── Osa0.4.png ├── Osa0.5.png └── Osa0.6.png ├── part_1 ├── anekdootit │ ├── .gitignore │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ └── manifest.json │ └── src │ │ └── index.js ├── kurssitiedot │ ├── .gitignore │ ├── README.md │ ├── index.js │ ├── package-lock.json │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ └── manifest.json │ └── src │ │ └── index.js └── unicafe │ ├── .gitignore │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── public │ ├── favicon.ico │ ├── index.html │ └── manifest.json │ └── src │ └── index.js ├── part_2 ├── kurssitiedot │ ├── .gitignore │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ └── manifest.json │ └── src │ │ ├── components │ │ └── Course.js │ │ └── index.js ├── puhelinluettelo │ ├── .gitignore │ ├── README.md │ ├── db.json │ ├── package-lock.json │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ └── manifest.json │ └── src │ │ ├── App.js │ │ ├── components │ │ ├── AddPerson.js │ │ ├── FilterPerson.js │ │ ├── Notification.js │ │ ├── Person.js │ │ └── Persons.js │ │ ├── index.css │ │ ├── index.js │ │ └── services │ │ └── personDB.js └── restcountries │ ├── .gitignore │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── public │ ├── favicon.ico │ ├── index.html │ └── manifest.json │ ├── src │ ├── App.js │ ├── components │ │ ├── Countries.js │ │ ├── Country.js │ │ ├── CountrySimple.js │ │ ├── FilterCountries.js │ │ └── Weather.js │ ├── index.js │ └── services │ │ ├── apixu.js │ │ └── restCountries.js │ └── yarn.lock ├── part_3 ├── .eslintignore ├── .eslintrc.js ├── Procfile ├── README.md ├── build │ ├── asset-manifest.json │ ├── favicon.ico │ ├── index.html │ ├── manifest.json │ ├── precache-manifest.29d722135ac602944601b19135bb7019.js │ ├── service-worker.js │ └── static │ │ ├── css │ │ ├── main.a7579a89.chunk.css │ │ └── main.a7579a89.chunk.css.map │ │ └── js │ │ ├── 1.23feea3a.chunk.js │ │ ├── 1.23feea3a.chunk.js.map │ │ ├── main.1c85c4e7.chunk.js │ │ ├── main.1c85c4e7.chunk.js.map │ │ ├── runtime~main.229c360f.js │ │ └── runtime~main.229c360f.js.map ├── index.js ├── models │ └── person.js ├── mongo.js ├── package-lock.json └── package.json ├── part_4 ├── .eslintrc.js ├── app.js ├── controllers │ ├── blogs.js │ ├── login.js │ └── users.js ├── index.js ├── jest.config.js ├── models │ ├── blog.js │ └── user.js ├── package-lock.json ├── package.json ├── tests │ ├── REST │ │ ├── create_user.REST │ │ ├── delete_blog.REST │ │ ├── login_user.REST │ │ └── post_blog.REST │ ├── blogs_api.test.js │ ├── dummy.test.js │ ├── totalLikes.test.js │ └── users_api.test.js └── utils │ ├── config.js │ ├── list_helper.js │ ├── logger.js │ └── middleware.js ├── part_5 ├── bloglist-backend │ ├── app.js │ ├── controllers │ │ ├── blogs.js │ │ ├── login.js │ │ └── users.js │ ├── create_user.REST │ ├── index.js │ ├── models │ │ ├── blog.js │ │ └── user.js │ ├── package-lock.json │ ├── package.json │ ├── tests │ │ ├── bloglist_api.test.js │ │ ├── list_helper.test.js │ │ └── test_helper.js │ └── utils │ │ ├── config.js │ │ ├── helper.js │ │ ├── list_helper.js │ │ └── middleware.js ├── bloglist-frontend │ ├── .eslintrc.js │ ├── .gitignore │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ └── manifest.json │ └── src │ │ ├── App.js │ │ ├── App.test.js │ │ ├── components │ │ ├── Blog.js │ │ ├── Blog.test.js │ │ ├── Notification.js │ │ ├── SimpleBlog.js │ │ ├── SimpleBlog.test.js │ │ └── Togglable.js │ │ ├── hooks │ │ └── index.js │ │ ├── index.css │ │ ├── index.js │ │ ├── services │ │ ├── __mocks__ │ │ │ └── blogs.js │ │ ├── blogs.js │ │ └── login.js │ │ └── setupTests.js └── custom-hooks │ ├── .gitignore │ ├── README.md │ ├── db.json │ ├── package-lock.json │ ├── package.json │ ├── public │ ├── favicon.ico │ ├── index.html │ └── manifest.json │ └── src │ ├── App.js │ └── index.js ├── part_6 ├── redux-anecdotes │ ├── .gitignore │ ├── db.json │ ├── package-lock.json │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ └── manifest.json │ └── src │ │ ├── App.js │ │ ├── components │ │ ├── .keep │ │ ├── AnecdoteForm.js │ │ ├── AnecdoteList.js │ │ ├── Filter.js │ │ └── Notification.js │ │ ├── index.js │ │ ├── reducers │ │ ├── anecdoteReducer.js │ │ ├── filterReducer.js │ │ └── notificationReducer.js │ │ ├── services │ │ └── anecdotes.js │ │ └── store.js └── unicafe-redux │ ├── .gitignore │ ├── package-lock.json │ ├── package.json │ ├── public │ ├── favicon.ico │ ├── index.html │ └── manifest.json │ └── src │ ├── index.js │ ├── reducer.js │ └── reducer.test.js ├── part_7 ├── bloglist-backend │ ├── app.js │ ├── controllers │ │ ├── blogs.js │ │ ├── login.js │ │ └── users.js │ ├── index.js │ ├── models │ │ ├── blog.js │ │ ├── jest.config.js │ │ └── user.js │ ├── package-lock.json │ ├── package.json │ ├── tests │ │ ├── bloglist_api.test.js │ │ ├── list_helper.test.js │ │ └── test_helper.js │ └── utils │ │ ├── config.js │ │ ├── helper.js │ │ ├── list_helper.js │ │ ├── middleware.js │ │ └── test_helper.js ├── bloglist-frontend │ ├── .eslintrc.js │ ├── package-lock.json │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ └── manifest.json │ └── src │ │ ├── App.js │ │ ├── components │ │ ├── Blog.js │ │ ├── BlogList.js │ │ ├── Login.js │ │ ├── NavBar.js │ │ ├── NewBlog.js │ │ ├── Notification.js │ │ ├── Togglable.js │ │ ├── User.js │ │ └── Users.js │ │ ├── hooks │ │ └── index.js │ │ ├── index.js │ │ ├── reducers │ │ ├── blogReducer.js │ │ ├── notificationReducer.js │ │ ├── userReducer.js │ │ └── usersReducer.js │ │ ├── services │ │ ├── blogs.js │ │ ├── login.js │ │ └── users.js │ │ └── store.js └── routed-anecdotes │ ├── .gitignore │ ├── package-lock.json │ ├── package.json │ ├── public │ ├── favicon.ico │ ├── index.html │ └── manifest.json │ └── src │ ├── App.js │ └── index.js ├── part_8 ├── library-backend │ ├── library-backend.js │ ├── models │ │ ├── author.js │ │ ├── book.js │ │ └── user.js │ ├── package-lock.json │ └── package.json └── library-frontend │ ├── .gitignore │ ├── package-lock.json │ ├── package.json │ ├── public │ ├── favicon.ico │ ├── index.html │ └── manifest.json │ └── src │ ├── App.js │ ├── components │ ├── Authors.js │ ├── Books.js │ ├── Login.js │ ├── NewBook.js │ └── RecommendedBooks.js │ └── index.js └── part_9 ├── exercises_9.1-9.7 ├── .eslintrc ├── bmiCalculator.ts ├── exerciseCalculator.ts ├── index.ts ├── package-lock.json ├── package.json └── tsconfig.json ├── patientor-backend ├── .eslintrc ├── .gitignore ├── data │ ├── diagnoses.json │ └── patients.json ├── index.ts ├── package-lock.json ├── package.json ├── tsconfig.json ├── typeguards.ts └── types.ts └── react-exercises ├── .eslintrc ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── src ├── App.tsx ├── index.tsx └── react-app-env.d.ts └── tsconfig.json /README.md: -------------------------------------------------------------------------------- 1 | ## [Full Stack Open](https://fullstackopen.com/en/) 2 | 3 | Frontend for the patientor project in round 9 is hosted [here](https://github.com/villeheikkila/patientor). 4 | 5 | ### Status 6 | 7 | | Part | Status | 8 | | ---- | ------ | 9 | | 0 | ✅ | 10 | | 1 | ✅ | 11 | | 2 | ✅ | 12 | | 3 | ✅ | 13 | | 4 | ✅ | 14 | | 5 | ✅ | 15 | | 6 | ✅ | 16 | | 7 | ✅ | 17 | | 8 | ✅ | 18 | | 9 | ✅ | 19 | -------------------------------------------------------------------------------- /part_0/Osa0.4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/villeheikkila/fullstackopen/7d62b2885996e6e68a96fb4cf0133fb371095767/part_0/Osa0.4.png -------------------------------------------------------------------------------- /part_0/Osa0.5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/villeheikkila/fullstackopen/7d62b2885996e6e68a96fb4cf0133fb371095767/part_0/Osa0.5.png -------------------------------------------------------------------------------- /part_0/Osa0.6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/villeheikkila/fullstackopen/7d62b2885996e6e68a96fb4cf0133fb371095767/part_0/Osa0.6.png -------------------------------------------------------------------------------- /part_1/anekdootit/.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 | -------------------------------------------------------------------------------- /part_1/anekdootit/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "anekdootit", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "react": "^16.8.6", 7 | "react-dom": "^16.8.6", 8 | "react-scripts": "3.0.1" 9 | }, 10 | "scripts": { 11 | "start": "react-scripts start", 12 | "build": "react-scripts build", 13 | "test": "react-scripts test", 14 | "eject": "react-scripts eject" 15 | }, 16 | "eslintConfig": { 17 | "extends": "react-app" 18 | }, 19 | "browserslist": [ 20 | ">0.2%", 21 | "not dead", 22 | "not ie <= 11", 23 | "not op_mini all" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /part_1/anekdootit/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/villeheikkila/fullstackopen/7d62b2885996e6e68a96fb4cf0133fb371095767/part_1/anekdootit/public/favicon.ico -------------------------------------------------------------------------------- /part_1/anekdootit/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 15 | 16 | 25 | React App 26 | 27 | 28 | 29 |
30 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /part_1/anekdootit/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /part_1/anekdootit/src/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import ReactDOM from "react-dom"; 3 | 4 | const Button = ({ handleClick, text }) => ( 5 | 6 | ); 7 | 8 | const App = props => { 9 | const [selected, setSelected] = useState(0); 10 | const [points, setPoints] = useState(new Uint8Array(6)); 11 | const [mostVoted, setMostVoted] = useState(""); 12 | 13 | const handleSelectedClick = () => { 14 | setSelected(Math.floor(Math.random() * anecdotes.length)); 15 | }; 16 | 17 | const handleVoteClick = () => { 18 | const copy = { ...points }; 19 | copy[selected] += 1; 20 | setPoints(copy); 21 | if (points[selected] > points[mostVoted]) { 22 | setMostVoted(selected); 23 | } 24 | }; 25 | 26 | return ( 27 |
28 |

Anecdote of the day

29 | {props.anecdotes[selected]} 30 |
31 | has {points[selected]} points 32 |
33 |
39 | ); 40 | }; 41 | 42 | const anecdotes = [ 43 | "If it hurts, do it more often", 44 | "Adding manpower to a late software project makes it later!", 45 | "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.", 46 | "Any fool can write code that a computer can understand. Good programmers write code that humans can understand.", 47 | "Premature optimization is the root of all evil.", 48 | "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." 49 | ]; 50 | 51 | ReactDOM.render(, document.getElementById("root")); 52 | -------------------------------------------------------------------------------- /part_1/kurssitiedot/.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 | -------------------------------------------------------------------------------- /part_1/kurssitiedot/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | 4 | const Header = (props) => { 5 | return ( 6 |
7 |

{props.course}

8 |
9 | ) 10 | } 11 | 12 | const Part = (props) => { 13 | return ( 14 |
15 |

{props.content.name} {props.content.exercises}

16 |
17 | ) 18 | } 19 | 20 | const Content = (props) => { 21 | return ( 22 |
23 |
24 |

25 | 26 | 27 | 28 |
29 |
30 | ) 31 | } 32 | 33 | const Total = (props) => { 34 | return ( 35 |
36 |

yhteensä {props.total[0].exercises + props.total[1].exercises + props.total[2].exercises} tehtävää

37 |
38 | ) 39 | } 40 | 41 | 42 | const App = () => { 43 | const course = { 44 | name: 'Half Stack -sovelluskehitys', 45 | parts: [ 46 | { 47 | name: 'Reactin perusteet', 48 | exercises: 10 49 | }, 50 | { 51 | name: 'Tiedonvälitys propseilla', 52 | exercises: 7 53 | }, 54 | { 55 | name: 'Komponenttien tila', 56 | exercises: 14 57 | } 58 | ] 59 | } 60 | 61 | return ( 62 |
63 |
64 | 65 | 66 |
67 | ) 68 | } 69 | 70 | ReactDOM.render( 71 | , 72 | document.getElementById('root') 73 | ) -------------------------------------------------------------------------------- /part_1/kurssitiedot/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kurssitiedot", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "react": "^16.8.6", 7 | "react-dom": "^16.8.6", 8 | "react-scripts": "3.0.1" 9 | }, 10 | "scripts": { 11 | "start": "react-scripts start", 12 | "build": "react-scripts build", 13 | "test": "react-scripts test", 14 | "eject": "react-scripts eject" 15 | }, 16 | "eslintConfig": { 17 | "extends": "react-app" 18 | }, 19 | "browserslist": [ 20 | ">0.2%", 21 | "not dead", 22 | "not ie <= 11", 23 | "not op_mini all" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /part_1/kurssitiedot/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/villeheikkila/fullstackopen/7d62b2885996e6e68a96fb4cf0133fb371095767/part_1/kurssitiedot/public/favicon.ico -------------------------------------------------------------------------------- /part_1/kurssitiedot/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 15 | 16 | 25 | React App 26 | 27 | 28 | 29 |
30 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /part_1/kurssitiedot/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /part_1/kurssitiedot/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | 4 | const Header = (props) => { 5 | return ( 6 |
7 |

{props.course}

8 |
9 | ) 10 | } 11 | 12 | const Part = (props) => { 13 | return ( 14 |
15 |

{props.content.name} {props.content.exercises}

16 |
17 | ) 18 | } 19 | 20 | const Content = (props) => { 21 | return ( 22 |
23 |
24 |

25 | 26 | 27 | 28 |
29 |
30 | ) 31 | } 32 | 33 | const Total = (props) => { 34 | return ( 35 |
36 |

yhteensä {props.total[0].exercises + props.total[1].exercises + props.total[2].exercises} tehtävää

37 |
38 | ) 39 | } 40 | 41 | 42 | const App = () => { 43 | const kurssi = { 44 | name: 'Half Stack -sovelluskehitys', 45 | parts: [ 46 | { 47 | name: 'Reactin perusteet', 48 | exercises: 10 49 | }, 50 | { 51 | name: 'Tiedonvälitys propseilla', 52 | exercises: 7 53 | }, 54 | { 55 | name: 'Komponenttien tila', 56 | exercises: 14 57 | } 58 | ] 59 | } 60 | 61 | return ( 62 |
63 |
64 | 65 | 66 |
67 | ) 68 | } 69 | 70 | ReactDOM.render( 71 | , 72 | document.getElementById('root') 73 | ) -------------------------------------------------------------------------------- /part_1/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 | -------------------------------------------------------------------------------- /part_1/unicafe/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "unicafe", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "react": "^16.8.6", 7 | "react-dom": "^16.8.6", 8 | "react-scripts": "3.0.1" 9 | }, 10 | "scripts": { 11 | "start": "react-scripts start", 12 | "build": "react-scripts build", 13 | "test": "react-scripts test", 14 | "eject": "react-scripts eject" 15 | }, 16 | "eslintConfig": { 17 | "extends": "react-app" 18 | }, 19 | "browserslist": [ 20 | ">0.2%", 21 | "not dead", 22 | "not ie <= 11", 23 | "not op_mini all" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /part_1/unicafe/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/villeheikkila/fullstackopen/7d62b2885996e6e68a96fb4cf0133fb371095767/part_1/unicafe/public/favicon.ico -------------------------------------------------------------------------------- /part_1/unicafe/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 15 | 16 | 25 | React App 26 | 27 | 28 | 29 |
30 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /part_1/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 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /part_1/unicafe/src/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import ReactDOM from 'react-dom' 3 | 4 | // oikea paikka komponentin määrittelyyn 5 | 6 | const Statistics = (props) => { 7 | // Ei heti tuu mitään fiksumpaa mieleen... 8 | if (props.summa === 0 && props.title === true) { 9 | return ( 10 | 11 | Ei yhtään palautetta annettu 12 | 13 | ) 14 | } 15 | 16 | if (props.summa === 0) { 17 | return ( 18 | 19 | 20 | 21 | ) 22 | } 23 | 24 | return ( 25 | 26 | {props.nimi} {props.sisalto} 27 | 28 | ) 29 | } 30 | 31 | const Button = ({ handleClick, text }) => ( 32 | 35 | ) 36 | 37 | const App = () => { 38 | // tallenna napit omaan tilaansa 39 | const [good, setGood] = useState(0) 40 | const [neutral, setNeutral] = useState(0) 41 | const [bad, setBad] = useState(0) 42 | 43 | const handleGoodClick = () => { 44 | setGood(good + 1) 45 | } 46 | 47 | const handleNeutralClick = () => { 48 | setNeutral(neutral + 1) 49 | } 50 | 51 | const handleBadClick = () => { 52 | setBad(bad + 1) 53 | } 54 | 55 | return ( 56 |
57 |
58 |

anna palautetta

59 |
75 |
76 | ) 77 | } 78 | 79 | ReactDOM.render(, 80 | document.getElementById('root') 81 | ) -------------------------------------------------------------------------------- /part_2/kurssitiedot/.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 | -------------------------------------------------------------------------------- /part_2/kurssitiedot/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kurssitiedot", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "axios": "^0.19.0", 7 | "react": "^16.8.6", 8 | "react-dom": "^16.8.6", 9 | "react-scripts": "3.0.1" 10 | }, 11 | "scripts": { 12 | "start": "react-scripts start", 13 | "build": "react-scripts build", 14 | "test": "react-scripts test", 15 | "eject": "react-scripts eject" 16 | }, 17 | "eslintConfig": { 18 | "extends": "react-app" 19 | }, 20 | "browserslist": [ 21 | ">0.2%", 22 | "not dead", 23 | "not ie <= 11", 24 | "not op_mini all" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /part_2/kurssitiedot/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/villeheikkila/fullstackopen/7d62b2885996e6e68a96fb4cf0133fb371095767/part_2/kurssitiedot/public/favicon.ico -------------------------------------------------------------------------------- /part_2/kurssitiedot/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 15 | 16 | 25 | React App 26 | 27 | 28 | 29 |
30 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /part_2/kurssitiedot/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /part_2/kurssitiedot/src/components/Course.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Header = props => 4 |

{props.course.name}

5 | 6 | const Total = (props) => { 7 | const parts = props.course.parts.map(course => course.exercises) 8 | 9 | return ( 10 |

yhteensä {parts.reduce((s, p) => s + p)} tehtävää

11 | ) 12 | } 13 | 14 | const Part = props => 15 |

{props.name} {props.exercises}

16 | 17 | const Content = (props) => { 18 | return ( 19 |
20 | {props.course.parts.map(part => )} 21 |
22 | ) 23 | } 24 | const Course = (props) => { 25 | console.log(props) 26 | 27 | return ( 28 |
29 |
30 | 31 | 32 |
33 | ) 34 | } 35 | 36 | export default Course -------------------------------------------------------------------------------- /part_2/kurssitiedot/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import Course from './components/Course.js' 4 | 5 | const App = () => { 6 | const courses = [ 7 | { 8 | name: 'Half Stack -sovelluskehitys', 9 | id: 1, 10 | parts: [ 11 | { 12 | name: 'Reactin perusteet', 13 | exercises: 10, 14 | id: 1 15 | }, 16 | { 17 | name: 'Tiedonvälitys propseilla', 18 | exercises: 7, 19 | id: 2 20 | }, 21 | { 22 | name: 'Komponenttien tila', 23 | exercises: 14, 24 | id: 3 25 | } 26 | ] 27 | }, 28 | { 29 | name: 'Node.js', 30 | id: 2, 31 | parts: [ 32 | { 33 | name: 'Routing', 34 | exercises: 3, 35 | id: 1 36 | }, 37 | { 38 | name: 'Middlewaret', 39 | exercises: 7, 40 | id: 2 41 | } 42 | ] 43 | } 44 | ] 45 | 46 | return ( 47 |
48 |

Opetusohjelma

49 | {courses.map(course => )} 50 |
51 | ) 52 | } 53 | 54 | ReactDOM.render( 55 | , 56 | document.getElementById('root') 57 | ) -------------------------------------------------------------------------------- /part_2/puhelinluettelo/.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 | -------------------------------------------------------------------------------- /part_2/puhelinluettelo/db.json: -------------------------------------------------------------------------------- 1 | { 2 | "persons": [ 3 | { 4 | "name": "Martti Tienari", 5 | "number": "040-123456", 6 | "id": 2 7 | }, 8 | { 9 | "name": "Arto Järvinen", 10 | "number": "040-123456", 11 | "id": 3 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /part_2/puhelinluettelo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "puhelinluettelo", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "axios": "^0.19.0", 7 | "react": "^16.8.6", 8 | "react-dom": "^16.8.6", 9 | "react-scripts": "3.0.1" 10 | }, 11 | "scripts": { 12 | "start": "react-scripts start", 13 | "build": "react-scripts build", 14 | "test": "react-scripts test", 15 | "eject": "react-scripts eject", 16 | "server": "json-server -p3001 db.json" 17 | }, 18 | "eslintConfig": { 19 | "extends": "react-app" 20 | }, 21 | "browserslist": [ 22 | ">0.2%", 23 | "not dead", 24 | "not ie <= 11", 25 | "not op_mini all" 26 | ], 27 | "devDependencies": { 28 | "json-server": "^0.15.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /part_2/puhelinluettelo/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/villeheikkila/fullstackopen/7d62b2885996e6e68a96fb4cf0133fb371095767/part_2/puhelinluettelo/public/favicon.ico -------------------------------------------------------------------------------- /part_2/puhelinluettelo/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 15 | 16 | 25 | React App 26 | 27 | 28 | 29 |
30 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /part_2/puhelinluettelo/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /part_2/puhelinluettelo/src/components/AddPerson.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const NewPerson = (props) => { 4 | return ( 5 |
6 | nimi: 7 | 10 |
11 | numero: 12 | 15 |
16 | 17 |
18 | ) 19 | } 20 | 21 | export default NewPerson -------------------------------------------------------------------------------- /part_2/puhelinluettelo/src/components/FilterPerson.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const FilterPerson = (props) => { 4 | return ( 5 |
6 | rajaa näytettäviä 7 | 10 |
11 |
12 | ) 13 | } 14 | 15 | export default FilterPerson -------------------------------------------------------------------------------- /part_2/puhelinluettelo/src/components/Notification.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | const Notification = ({ message }) => { 3 | if (message === null) { 4 | return null 5 | } 6 | 7 | return ( 8 |
9 | {message} 10 |
11 | ) 12 | } 13 | 14 | export default Notification; -------------------------------------------------------------------------------- /part_2/puhelinluettelo/src/components/Person.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Person = (props, deletePerson) => { 4 | return ( 5 |
  • {props.name} {props.number}
  • 6 | ) 7 | } 8 | 9 | export default Person -------------------------------------------------------------------------------- /part_2/puhelinluettelo/src/components/Persons.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import Person from './Person' 4 | 5 | const Persons = (props) => { 6 | return ( 7 |
      8 | {props.persons.filter(person => person.name.toUpperCase().includes(props.newSearch.toUpperCase())).map(person => ( 9 | ))} 10 |
    11 | ) 12 | } 13 | 14 | export default Persons -------------------------------------------------------------------------------- /part_2/puhelinluettelo/src/index.css: -------------------------------------------------------------------------------- 1 | .error { 2 | color: red; 3 | background: lightgrey; 4 | font-size: 20px; 5 | border-style: solid; 6 | border-radius: 5px; 7 | padding: 10px; 8 | margin-bottom: 10px; 9 | } -------------------------------------------------------------------------------- /part_2/puhelinluettelo/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import App from './App' 4 | import './index.css' 5 | 6 | ReactDOM.render( 7 | , 8 | document.getElementById('root') 9 | ) -------------------------------------------------------------------------------- /part_2/puhelinluettelo/src/services/personDB.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | const baseUrl = "http://localhost:3001/persons"; 3 | //const baseUrl = '/api/persons' 4 | 5 | const getAll = () => { 6 | const request = axios.get(baseUrl); 7 | return request.then(response => response.data); 8 | }; 9 | 10 | const create = newObject => { 11 | const request = axios.post(baseUrl, newObject); 12 | return request.then(response => response.data); 13 | }; 14 | 15 | const update = (id, newObject) => { 16 | const request = axios.put(`${baseUrl}/${id}`, newObject); 17 | return request.then(response => response.data); 18 | }; 19 | 20 | const deletePerson = id => { 21 | const request = axios.delete(`${baseUrl}/${id}`); 22 | return request.then(response => response.data); 23 | }; 24 | 25 | export default { 26 | getAll: getAll, 27 | create: create, 28 | update: update, 29 | deletePerson: deletePerson 30 | }; 31 | -------------------------------------------------------------------------------- /part_2/restcountries/.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 | -------------------------------------------------------------------------------- /part_2/restcountries/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "restcountries", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "axios": "^0.19.0", 7 | "react": "^16.8.6", 8 | "react-dom": "^16.8.6", 9 | "react-scripts": "3.0.1" 10 | }, 11 | "scripts": { 12 | "start": "react-scripts start", 13 | "build": "react-scripts build", 14 | "test": "react-scripts test", 15 | "eject": "react-scripts eject" 16 | }, 17 | "eslintConfig": { 18 | "extends": "react-app" 19 | }, 20 | "browserslist": { 21 | "production": [ 22 | ">0.2%", 23 | "not dead", 24 | "not op_mini all" 25 | ], 26 | "development": [ 27 | "last 1 chrome version", 28 | "last 1 firefox version", 29 | "last 1 safari version" 30 | ] 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /part_2/restcountries/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/villeheikkila/fullstackopen/7d62b2885996e6e68a96fb4cf0133fb371095767/part_2/restcountries/public/favicon.ico -------------------------------------------------------------------------------- /part_2/restcountries/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 22 | React App 23 | 24 | 25 | 26 |
    27 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /part_2/restcountries/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /part_2/restcountries/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import getAll from "./services/restCountries"; 3 | import FilterCountries from "./components/FilterCountries"; 4 | import Countries from "./components/Countries"; 5 | 6 | function App() { 7 | const [newSearch, setNewSearch] = useState(""); 8 | const [countries, setCountries] = useState([]); 9 | 10 | const handleSearchChange = event => { 11 | setNewSearch(event.target.value); 12 | }; 13 | 14 | useEffect(() => { 15 | getAll().then(response => setCountries(response)); 16 | }, []); 17 | 18 | return ( 19 |
    20 | 24 | 25 |
    26 | ); 27 | } 28 | 29 | export default App; 30 | -------------------------------------------------------------------------------- /part_2/restcountries/src/components/Countries.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import Country from "./Country"; 3 | import CountrySimple from "./CountrySimple"; 4 | 5 | const Countries = ({ countries, newSearch }) => { 6 | const [showCountry, setShowContry] = useState(); 7 | 8 | const show = event => { 9 | console.log(event.target.value); 10 | const cont = countries.filter(country => 11 | country.name.includes(event.target.value) 12 | ); 13 | console.log("cont: ", cont); 14 | setShowContry(cont[0]); 15 | }; 16 | 17 | const entries = countries.filter(country => 18 | country.name.toUpperCase().includes(newSearch.toUpperCase()) 19 | ); 20 | 21 | if (entries.length >= 10) { 22 | return

    Too many matches, specify another filter

    ; 23 | } 24 | if (showCountry !== undefined) { 25 | return ( 26 | 34 | ); 35 | } 36 | if (entries.length > 1) { 37 | return ( 38 |
      39 | {countries 40 | .filter(country => 41 | country.name.toUpperCase().includes(newSearch.toUpperCase()) 42 | ) 43 | .map(country => ( 44 | 50 | ))} 51 |
    52 | ); 53 | } 54 | 55 | return ( 56 |
      57 | {countries 58 | .filter(country => 59 | country.name.toUpperCase().includes(newSearch.toUpperCase()) 60 | ) 61 | .map(country => ( 62 | 70 | ))} 71 |
    72 | ); 73 | }; 74 | 75 | export default Countries; 76 | -------------------------------------------------------------------------------- /part_2/restcountries/src/components/Country.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import Weather from "./Weather"; 3 | 4 | const Country = ({ name, capital, population, languages, flagUrl }) => { 5 | return ( 6 |
    7 |

    {name}

    8 |

    capital {capital}

    9 |

    population {population}

    10 |

    languages

    11 |
      12 | {languages.map(language => ( 13 |
    • {language.name}
    • 14 | ))} 15 |
    16 | No flag found 17 | 18 |
    19 | ); 20 | }; 21 | 22 | export default Country; 23 | -------------------------------------------------------------------------------- /part_2/restcountries/src/components/CountrySimple.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | 3 | const Country = ({ name, country, show }) => { 4 | return ( 5 |
  • 6 | {name} 7 | 10 |
  • 11 | ); 12 | }; 13 | 14 | export default Country; 15 | -------------------------------------------------------------------------------- /part_2/restcountries/src/components/FilterCountries.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const FilterCountries = ({ newSearch, handleSearchChange }) => { 4 | return ( 5 |
    6 | find countries 7 | 8 |
    9 |
    10 | ); 11 | }; 12 | 13 | export default FilterCountries; 14 | -------------------------------------------------------------------------------- /part_2/restcountries/src/components/Weather.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import getWeather from "../services/apixu"; 3 | 4 | const Weather = ({ capital }) => { 5 | const [weather, setWeather] = useState(); 6 | getWeather(capital).then(e => setWeather(e)); 7 | 8 | if (weather === undefined) { 9 | return

    Loading...

    ; 10 | } else { 11 | console.log(weather); 12 | const temp = weather.temp_c; 13 | const conditionURL = `http:${weather.condition.icon}`; 14 | const wind = weather.wind_kph; 15 | const windDirection = weather.wind_dir; 16 | return ( 17 |
    18 | temperature {temp} 19 |
    20 | Loading... 21 |
    22 | wind {wind} direction {windDirection} 23 |
    24 | ); 25 | } 26 | }; 27 | export default Weather; 28 | -------------------------------------------------------------------------------- /part_2/restcountries/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 | -------------------------------------------------------------------------------- /part_2/restcountries/src/services/apixu.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | const baseUrl = 3 | "http://api.apixu.com/v1/current.json?key=d6be1fd112814c5481c11105190706&q="; 4 | 5 | const getWeather = async capital => { 6 | const response = await axios.get(`${baseUrl}${capital}`); 7 | return response.data.current; 8 | }; 9 | 10 | export default getWeather; 11 | -------------------------------------------------------------------------------- /part_2/restcountries/src/services/restCountries.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | const baseUrl = "https://restcountries.eu/rest/v2/all"; 3 | 4 | const getAll = async () => { 5 | const response = await axios.get(baseUrl); 6 | return response.data; 7 | }; 8 | 9 | export default getAll; 10 | -------------------------------------------------------------------------------- /part_3/.eslintignore: -------------------------------------------------------------------------------- 1 | build -------------------------------------------------------------------------------- /part_3/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es6: true 5 | }, 6 | extends: "eslint:recommended", 7 | parserOptions: { 8 | ecmaVersion: 2018 9 | }, 10 | rules: { 11 | indent: ["error", 2], 12 | "linebreak-style": ["error", "linux"], 13 | quotes: ["error", "single"], 14 | semi: ["error", "never"], 15 | eqeqeq: "error", 16 | "no-trailing-spaces": "error", 17 | "object-curly-spacing": ["error", "always"], 18 | "arrow-spacing": ["error", { before: true, after: true }], 19 | "no-console": 0 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /part_3/Procfile: -------------------------------------------------------------------------------- 1 | web: node index.js -------------------------------------------------------------------------------- /part_3/README.md: -------------------------------------------------------------------------------- 1 | ## Sovelluksen osoite Herokussa 2 | 3 | https://pure-falls-78176.herokuapp.com/ 4 | 5 | ## Päivitetty frontend löytyy GitHubista: 6 | 7 | https://github.com/villeheikkila/fullstack -------------------------------------------------------------------------------- /part_3/build/asset-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "main.css": "/static/css/main.a7579a89.chunk.css", 3 | "main.js": "/static/js/main.1c85c4e7.chunk.js", 4 | "main.js.map": "/static/js/main.1c85c4e7.chunk.js.map", 5 | "static/js/1.23feea3a.chunk.js": "/static/js/1.23feea3a.chunk.js", 6 | "static/js/1.23feea3a.chunk.js.map": "/static/js/1.23feea3a.chunk.js.map", 7 | "runtime~main.js": "/static/js/runtime~main.229c360f.js", 8 | "runtime~main.js.map": "/static/js/runtime~main.229c360f.js.map", 9 | "static/css/main.a7579a89.chunk.css.map": "/static/css/main.a7579a89.chunk.css.map", 10 | "index.html": "/index.html", 11 | "precache-manifest.29d722135ac602944601b19135bb7019.js": "/precache-manifest.29d722135ac602944601b19135bb7019.js", 12 | "service-worker.js": "/service-worker.js" 13 | } -------------------------------------------------------------------------------- /part_3/build/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/villeheikkila/fullstackopen/7d62b2885996e6e68a96fb4cf0133fb371095767/part_3/build/favicon.ico -------------------------------------------------------------------------------- /part_3/build/index.html: -------------------------------------------------------------------------------- 1 | React App
    -------------------------------------------------------------------------------- /part_3/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 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /part_3/build/precache-manifest.29d722135ac602944601b19135bb7019.js: -------------------------------------------------------------------------------- 1 | self.__precacheManifest = [ 2 | { 3 | "revision": "1c85c4e7aef97eb9c086", 4 | "url": "/static/css/main.a7579a89.chunk.css" 5 | }, 6 | { 7 | "revision": "1c85c4e7aef97eb9c086", 8 | "url": "/static/js/main.1c85c4e7.chunk.js" 9 | }, 10 | { 11 | "revision": "23feea3aedfdbea1a84a", 12 | "url": "/static/js/1.23feea3a.chunk.js" 13 | }, 14 | { 15 | "revision": "229c360febb4351a89df", 16 | "url": "/static/js/runtime~main.229c360f.js" 17 | }, 18 | { 19 | "revision": "26a054e94eccf0888dffcc60bbb63af4", 20 | "url": "/index.html" 21 | } 22 | ]; -------------------------------------------------------------------------------- /part_3/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/3.6.3/workbox-sw.js"); 15 | 16 | importScripts( 17 | "/precache-manifest.29d722135ac602944601b19135bb7019.js" 18 | ); 19 | 20 | workbox.clientsClaim(); 21 | 22 | /** 23 | * The workboxSW.precacheAndRoute() method efficiently caches and responds to 24 | * requests for URLs in the manifest. 25 | * See https://goo.gl/S9QRab 26 | */ 27 | self.__precacheManifest = [].concat(self.__precacheManifest || []); 28 | workbox.precaching.suppressWarnings(); 29 | workbox.precaching.precacheAndRoute(self.__precacheManifest, {}); 30 | 31 | workbox.routing.registerNavigationRoute("/index.html", { 32 | 33 | blacklist: [/^\/_/,/\/[^\/]+\.[^\/]+$/], 34 | }); 35 | -------------------------------------------------------------------------------- /part_3/build/static/css/main.a7579a89.chunk.css: -------------------------------------------------------------------------------- 1 | .error{color:red;background:#d3d3d3;font-size:20px;border-style:solid;border-radius:5px;padding:10px;margin-bottom:10px} 2 | /*# sourceMappingURL=main.a7579a89.chunk.css.map */ -------------------------------------------------------------------------------- /part_3/build/static/css/main.a7579a89.chunk.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["C:/Users/Ville/Documents/GitHub/Fullstack/Osa_2/puhelinluettelo/src/C:/Users/Ville/Documents/GitHub/Fullstack/Osa_2/puhelinluettelo/src/index.css","main.a7579a89.chunk.css"],"names":[],"mappings":"AAAA,OACI,SAAA,CACA,kBAAA,CACA,cAAA,CACA,kBAAA,CACA,iBAAA,CACA,YAAA,CACA,kBCCF","file":"main.a7579a89.chunk.css","sourcesContent":[".error {\r\n color: red;\r\n background: lightgrey;\r\n font-size: 20px;\r\n border-style: solid;\r\n border-radius: 5px;\r\n padding: 10px;\r\n margin-bottom: 10px;\r\n }",".error {\r\n color: red;\r\n background: lightgrey;\r\n font-size: 20px;\r\n border-style: solid;\r\n border-radius: 5px;\r\n padding: 10px;\r\n margin-bottom: 10px;\r\n }\n"]} -------------------------------------------------------------------------------- /part_3/build/static/js/runtime~main.229c360f.js: -------------------------------------------------------------------------------- 1 | !function(e){function r(r){for(var n,f,i=r[0],l=r[1],a=r[2],c=0,s=[];c { 11 | console.log('connected to MongoDB') 12 | }).catch((error) => { 13 | console.log('error connection to MongoDB:', error.message) 14 | }) 15 | 16 | 17 | const personSchema = new mongoose.Schema({ 18 | name: { 19 | type: String, 20 | minlength: 3, 21 | required: true, 22 | unique: true 23 | }, 24 | number: { 25 | type: String, 26 | minlength: 8, 27 | required: true, 28 | unique: true 29 | } 30 | }) 31 | 32 | personSchema.plugin(uniqueValidator); 33 | 34 | 35 | personSchema.set('toJSON', { 36 | transform: (document, returnedObject) => { 37 | returnedObject.id = returnedObject._id 38 | delete returnedObject._id 39 | delete returnedObject.__v 40 | } 41 | }) 42 | 43 | module.exports = mongoose.model('Person', personSchema) -------------------------------------------------------------------------------- /part_3/mongo.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | 3 | if (process.argv.length < 3) { 4 | console.log('give password as argument') 5 | process.exit(1) 6 | } 7 | 8 | const password = process.argv[2] 9 | const nameInput = process.argv[3] 10 | const numberInput = process.argv[4] 11 | 12 | 13 | const url = `mongodb+srv://puhelinluettelo:${password}@puhelinluettelo-me0oy.mongodb.net/test?retryWrites=true` 14 | 15 | mongoose.connect(url, { 16 | useNewUrlParser: true 17 | }) 18 | 19 | 20 | const personSchema = new mongoose.Schema({ 21 | name: String, 22 | number: String, 23 | }) 24 | 25 | const Person = mongoose.model('Person', personSchema) 26 | 27 | const person = new Person({ 28 | name: nameInput, 29 | number: numberInput 30 | }) 31 | 32 | if ((nameInput != undefined) && (numberInput != undefined)) { 33 | person.save().then(result => { 34 | console.log(`lisätään ${nameInput} numero ${numberInput} luetteloon`) 35 | mongoose.connection.close() 36 | }) 37 | } else { 38 | Person.find({}).then(result => { 39 | result.forEach(person => { 40 | console.log(person.name + " " + person.number) 41 | }) 42 | mongoose.connection.close() 43 | }) 44 | } -------------------------------------------------------------------------------- /part_3/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "puhelinluettelo", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index.js", 8 | "watch": "nodemon index.js", 9 | "test": "echo \"Error: no test specified\" && exit 1", 10 | "lint": "eslint ." 11 | }, 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "cors": "^2.8.5", 16 | "dotenv": "^8.0.0", 17 | "express": "^4.17.1", 18 | "mongoose": "^5.5.15", 19 | "mongoose-unique-validator": "^2.0.3", 20 | "morgan": "^1.9.1" 21 | }, 22 | "devDependencies": { 23 | "eslint": "^5.16.0", 24 | "nodemon": "^1.19.1" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /part_4/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | es6: true, 4 | node: true, 5 | jest: true 6 | }, 7 | extends: 'eslint:recommended', 8 | parserOptions: { 9 | ecmaVersion: 2018 10 | }, 11 | extends: 'eslint:recommended', 12 | rules: { 13 | indent: ['error', 2], 14 | 'linebreak-style': ['error', 'unix'], 15 | quotes: ['error', 'single'], 16 | semi: ['error', 'never'], 17 | eqeqeq: 'error', 18 | 'no-trailing-spaces': 'error', 19 | 'object-curly-spacing': ['error', 'always'], 20 | 'arrow-spacing': ['error', { before: true, after: true }], 21 | 'no-console': 0 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /part_4/app.js: -------------------------------------------------------------------------------- 1 | const config = require('./utils/config') 2 | const express = require('express') 3 | const bodyParser = require('body-parser') 4 | const app = express() 5 | const blogsRouter = require('./controllers/blogs') 6 | const usersRouter = require('./controllers/users') 7 | const loginRouter = require('./controllers/login') 8 | const middleware = require('./utils/middleware') 9 | const mongoose = require('mongoose') 10 | const logger = require('./utils/logger') 11 | 12 | logger.info('connecting to', config.MONGODB_URI) 13 | 14 | mongoose 15 | .connect(config.MONGODB_URI, { useNewUrlParser: true }) 16 | .then(() => { 17 | logger.info('connected to MongoDB') 18 | }) 19 | .catch(error => { 20 | logger.error('error connection to MongoDB:', error.message) 21 | }) 22 | app.use(middleware.tokenExtractor) 23 | 24 | app.use(express.static('build')) 25 | app.use(bodyParser.json()) 26 | app.use(middleware.requestLogger) 27 | 28 | app.use('/api/blogs', blogsRouter) 29 | app.use('/api/users', usersRouter) 30 | app.use('/api/login', loginRouter) 31 | 32 | app.use(middleware.unknownEndpoint) 33 | app.use(middleware.errorHandler) 34 | 35 | module.exports = app 36 | -------------------------------------------------------------------------------- /part_4/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 = 11 | user === null 12 | ? false 13 | : await bcrypt.compare(body.password, user.passwordHash) 14 | 15 | if (!(user && passwordCorrect)) { 16 | return response.status(401).json({ 17 | error: 'invalid username or password' 18 | }) 19 | } 20 | 21 | const userForToken = { 22 | username: user.username, 23 | id: user._id 24 | } 25 | 26 | const token = jwt.sign(userForToken, process.env.SECRET) 27 | 28 | response.status(200).send({ token, username: user.username, name: user.name }) 29 | }) 30 | 31 | module.exports = loginRouter 32 | -------------------------------------------------------------------------------- /part_4/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, next) => { 6 | try { 7 | const body = request.body 8 | 9 | if (body.password === undefined || body.password.length < 3) { 10 | return response 11 | .status(400) 12 | .json({ error: 'password is too short or missing' }) 13 | } 14 | 15 | const saltRounds = 10 16 | const passwordHash = await bcrypt.hash(body.password, saltRounds) 17 | 18 | const user = new User({ 19 | username: body.username, 20 | name: body.name, 21 | passwordHash 22 | }) 23 | 24 | const savedUser = await user.save() 25 | 26 | response.json(savedUser) 27 | } catch (exception) { 28 | next(exception) 29 | } 30 | }) 31 | 32 | usersRouter.get('/', async (request, response, next) => { 33 | try { 34 | const users = await User.find({}).populate('blogs') 35 | response.json(users) 36 | } catch (exception) { 37 | next(exception) 38 | } 39 | }) 40 | 41 | module.exports = usersRouter 42 | -------------------------------------------------------------------------------- /part_4/index.js: -------------------------------------------------------------------------------- 1 | const http = require('http') 2 | const app = require('./app') 3 | const config = require('./utils/config') 4 | const server = http.createServer(app) 5 | 6 | server.listen(config.PORT, () => { 7 | console.log(`Server running on port ${config.PORT}`) 8 | }) -------------------------------------------------------------------------------- /part_4/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node' 3 | } -------------------------------------------------------------------------------- /part_4/models/blog.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | 3 | const blogSchema = mongoose.Schema({ 4 | title: String, 5 | author: String, 6 | url: String, 7 | likes: Number, 8 | user: { 9 | type: mongoose.Schema.Types.ObjectId, 10 | ref: 'User' 11 | } 12 | }) 13 | 14 | blogSchema.set('toJSON', { 15 | transform: (document, returnedObject) => { 16 | returnedObject.id = returnedObject._id 17 | delete returnedObject._id 18 | delete returnedObject.__v 19 | } 20 | }) 21 | 22 | module.exports = mongoose.model('Blog', blogSchema) 23 | -------------------------------------------------------------------------------- /part_4/models/user.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | const uniqueValidator = require('mongoose-unique-validator') 3 | 4 | const userSchema = mongoose.Schema({ 5 | username: { 6 | type: String, 7 | minlength: 3, 8 | required: true, 9 | unique: true 10 | }, 11 | name: String, 12 | passwordHash: String, 13 | blogs: [ 14 | { 15 | type: mongoose.Schema.Types.ObjectId, 16 | ref: 'Blog' 17 | } 18 | ] 19 | }) 20 | 21 | userSchema.plugin(uniqueValidator) 22 | 23 | userSchema.set('toJSON', { 24 | transform: (document, returnedObject) => { 25 | returnedObject.id = returnedObject._id.toString() 26 | delete returnedObject._id 27 | delete returnedObject.__v 28 | delete returnedObject.passwordHash 29 | } 30 | }) 31 | 32 | const User = mongoose.model('User', userSchema) 33 | 34 | module.exports = User 35 | -------------------------------------------------------------------------------- /part_4/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "blogilista", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "cross-env NODE_ENV=production node index.js", 8 | "watch": "cross-env NODE_ENV=development nodemon index.js", 9 | "test": "cross-env NODE_ENV=test jest --verbose --runInBand", 10 | "lint": "eslint ." 11 | }, 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "bcrypt": "^3.0.6", 16 | "cors": "^2.8.5", 17 | "dotenv": "^8.0.0", 18 | "express": "^4.17.1", 19 | "jsonwebtoken": "^8.5.1", 20 | "mongoose": "^5.5.15", 21 | "mongoose-unique-validator": "^2.0.3" 22 | }, 23 | "devDependencies": { 24 | "cross-env": "^5.2.0", 25 | "eslint": "^5.16.0", 26 | "jest": "^24.8.0", 27 | "nodemon": "^1.19.1", 28 | "supertest": "^4.0.2" 29 | }, 30 | "jest": { 31 | "testEnvironment": "node" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /part_4/tests/REST/create_user.REST: -------------------------------------------------------------------------------- 1 | POST http://localhost:3001/api/users 2 | Content-Type: application/json 3 | 4 | { 5 | "username": "moi", 6 | "password": "moi", 7 | "name": "moi" 8 | } -------------------------------------------------------------------------------- /part_4/tests/REST/delete_blog.REST: -------------------------------------------------------------------------------- 1 | DELETE http://localhost:3001/api/blogs/5d019169005b3c729a0eb8d1 2 | Authorization: bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im1vaSIsImlkIjoiNWQwMTkxODcwMDViM2M3MjlhMGViOGQzIiwiaWF0IjoxNTYwMzg0MDcwfQ.Dxu6tCuZWmCm3I7s8cs6t5TUD_WNNDE0-1zCFzp2M7Q 3 | -------------------------------------------------------------------------------- /part_4/tests/REST/login_user.REST: -------------------------------------------------------------------------------- 1 | POST http://localhost:3001/api/login 2 | Content-Type: application/json 3 | 4 | { 5 | "username": "moi", 6 | "password": "moi" 7 | } -------------------------------------------------------------------------------- /part_4/tests/REST/post_blog.REST: -------------------------------------------------------------------------------- 1 | POST http://localhost:3001/api/blogs 2 | Content-Type: application/json 3 | Authorization: bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im1vaWtrYWEiLCJpZCI6IjVkMDE4OWM2YThhYTBhNjdkNjQ2ODJjNCIsImlhdCI6MTU2MDM4MjUwN30.y6zWaH4pGhMraLBPB9A0kVIBSiExrpk3svO5m42SYqI 4 | 5 | { 6 | "title": "titteli", 7 | "author": "authaa", 8 | "url": "urli", 9 | "likes": 1 10 | } -------------------------------------------------------------------------------- /part_4/tests/dummy.test.js: -------------------------------------------------------------------------------- 1 | const listHelper = require('../utils/list_helper') 2 | 3 | test('dummy returns one', () => { 4 | const blogs = [] 5 | 6 | const result = listHelper.dummy(blogs) 7 | expect(result).toBe(1) 8 | }) 9 | -------------------------------------------------------------------------------- /part_4/tests/users_api.test.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | const supertest = require('supertest') 3 | const app = require('../app') 4 | const User = require('../models/user') 5 | const api = supertest(app) 6 | 7 | const initialUser = { 8 | username: 'username', 9 | passwordHash: 'password', 10 | name: 'name' 11 | } 12 | 13 | beforeEach(async () => { 14 | await User.deleteMany({}) 15 | 16 | const userObject = new User(initialUser) 17 | await userObject.save() 18 | }) 19 | 20 | test('GET /api/users id is id not _id', async () => { 21 | try { 22 | const response = await api.get('/api/users') 23 | expect(response.body[0].id).toBe(24) 24 | } catch (e) { 25 | console.log('error', e) 26 | } 27 | }) 28 | 29 | test('GET /api/users user._id is not set', async () => { 30 | try { 31 | const response = await api.get('/api/users') 32 | expect(response.body[0]._id).toBe(undefined) 33 | } catch (e) { 34 | console.log('error', e) 35 | } 36 | }) 37 | 38 | afterAll(() => { 39 | mongoose.connection.close() 40 | }) 41 | -------------------------------------------------------------------------------- /part_4/utils/config.js: -------------------------------------------------------------------------------- 1 | if (process.env.NODE_ENV !== 'production') { 2 | require('dotenv').config() 3 | } 4 | 5 | let PORT = process.env.PORT 6 | let MONGODB_URI = process.env.MONGODB_URI 7 | 8 | 9 | if (process.env.NODE_ENV === 'test') { 10 | MONGODB_URI = process.env.TEST_MONGODB_URI 11 | } 12 | 13 | module.exports = { 14 | MONGODB_URI, 15 | PORT 16 | } -------------------------------------------------------------------------------- /part_4/utils/list_helper.js: -------------------------------------------------------------------------------- 1 | const dummy = blogs => { 2 | return 1 3 | } 4 | const totalLikes = blogs => { 5 | return blogs.reduce((total, blogi) => total + blogi.likes, 0) 6 | } 7 | 8 | const favoriteBlog = blogs => { 9 | return blogs.reduce((a, b) => (a.likes > b.likes ? a : b)) 10 | } 11 | 12 | const mostBlogs = blogs => { 13 | const result = blogs.reduce((a, b) => { 14 | let known = a.find(found => { 15 | return found.author === b.author 16 | }) 17 | 18 | if (!known) { 19 | return a.concat({ author: b.author, blogs: 1 }) 20 | } 21 | 22 | known.blogs++ 23 | return a 24 | }, []) 25 | 26 | return result.reduce((a, b) => (a.blogs > b.blogs ? a : b)) 27 | } 28 | 29 | const mostLikes = blogs => { 30 | const result = blogs.reduce((a, b) => { 31 | let known = a.find(found => { 32 | return found.author === b.author 33 | }) 34 | 35 | if (!known) { 36 | return a.concat({ author: b.author, likes: b.likes }) 37 | } 38 | 39 | known.likes += b.likes 40 | return a 41 | }, []) 42 | 43 | return favoriteBlog(result) 44 | } 45 | module.exports = { 46 | dummy, 47 | totalLikes, 48 | mostLikes, 49 | mostBlogs, 50 | favoriteBlog 51 | } 52 | -------------------------------------------------------------------------------- /part_4/utils/logger.js: -------------------------------------------------------------------------------- 1 | const info = (...params) => { 2 | if (process.env.NODE_ENV !== 'test') { 3 | console.log(...params) 4 | } 5 | } 6 | 7 | const error = (...params) => { 8 | console.error(...params) 9 | } 10 | 11 | module.exports = { 12 | info, 13 | error 14 | } 15 | -------------------------------------------------------------------------------- /part_4/utils/middleware.js: -------------------------------------------------------------------------------- 1 | const logger = require('./logger') 2 | 3 | const requestLogger = (request, response, next) => { 4 | logger.info('Method:', request.method) 5 | logger.info('Path: ', request.path) 6 | logger.info('Body: ', request.body) 7 | logger.info('---') 8 | next() 9 | } 10 | 11 | const unknownEndpoint = (request, response) => { 12 | response.status(404).send({ error: 'unknown endpoint' }) 13 | } 14 | 15 | const errorHandler = (error, request, response, next) => { 16 | logger.error(error.message) 17 | 18 | if (error.name === 'CastError' && error.kind === 'ObjectId') { 19 | return response.status(400).send({ error: 'malformatted id' }) 20 | } else if (error.name === 'ValidationError') { 21 | return response.status(400).json({ error: error.message }) 22 | } else if (error.name === 'JsonWebTokenError') { 23 | return response.status(401).json({ error: 'invalid token' }) 24 | } 25 | 26 | next(error) 27 | } 28 | 29 | const tokenExtractor = (request, response, next) => { 30 | const authorization = request.get('authorization') 31 | if (authorization && authorization.toLowerCase().startsWith('bearer ')) { 32 | request.token = authorization.substring(7) 33 | } 34 | next() 35 | } 36 | 37 | module.exports = { 38 | requestLogger, 39 | unknownEndpoint, 40 | errorHandler, 41 | tokenExtractor 42 | } 43 | -------------------------------------------------------------------------------- /part_5/bloglist-backend/app.js: -------------------------------------------------------------------------------- 1 | const config = require('./utils/config') 2 | const express = require('express') 3 | const app = express() 4 | const bodyParser = require('body-parser') 5 | const cors = require('cors') 6 | const mongoose = require('mongoose') 7 | 8 | const { tokenExtractor, errorHandler } = require('./utils/middleware') 9 | 10 | const loginRouter = require('./controllers/login') 11 | const blogsRouter = require('./controllers/blogs') 12 | const usersRouter = require('./controllers/users') 13 | 14 | app.use(cors()) 15 | app.use(bodyParser.json()) 16 | 17 | console.log('connecting to', config.MONGODB_URI) 18 | 19 | mongoose.connect(config.MONGODB_URI, { useNewUrlParser: true }) 20 | .then(() => { 21 | console.log('connected to MongoDB') 22 | }) 23 | .catch((error) => { 24 | console.log('error connection to MongoDB:', error.message) 25 | }) 26 | 27 | app.use(tokenExtractor) 28 | 29 | app.use('/api/login', loginRouter) 30 | app.use('/api/blogs', blogsRouter) 31 | app.use('/api/users', usersRouter) 32 | 33 | app.use(errorHandler) 34 | 35 | 36 | module.exports = app -------------------------------------------------------------------------------- /part_5/bloglist-backend/controllers/login.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken') 2 | const bcrypt = require('bcrypt') 3 | const loginRouter = require('express').Router() 4 | const User = require('../models/user') 5 | 6 | loginRouter.post('/', async (request, response) => { 7 | const body = request.body 8 | 9 | const user = await User.findOne({ username: body.username }) 10 | const passwordCorrect = 11 | user === null 12 | ? false 13 | : await bcrypt.compare(body.password, user.passwordHash) 14 | 15 | console.log(user) 16 | 17 | if (!(user && passwordCorrect)) { 18 | return response.status(401).json({ 19 | error: 'invalid username or password' 20 | }) 21 | } 22 | 23 | const userForToken = { 24 | username: user.username, 25 | id: user._id, 26 | } 27 | 28 | const token = jwt.sign(userForToken, process.env.SECRET) 29 | 30 | response 31 | .status(200) 32 | .send({ token, username: user.username, name: user.name }) 33 | }) 34 | 35 | module.exports = loginRouter -------------------------------------------------------------------------------- /part_5/bloglist-backend/controllers/users.js: -------------------------------------------------------------------------------- 1 | const bcrypt = require('bcrypt') 2 | const usersRouter = require('express').Router() 3 | const User = require('../models/user') 4 | 5 | usersRouter.get('/', async (request, response) => { 6 | const users = await User 7 | .find({}).populate('blogs', { author: 1, title: 1, url: 1 }) 8 | 9 | response.json(users.map(u => u.toJSON())) 10 | }) 11 | 12 | usersRouter.post('/', async (request, response, next) => { 13 | try { 14 | const { username, password, name } = request.body 15 | 16 | if (!password || password.length<3 ) { 17 | return response.status(400).send({ 18 | error: 'pasword minimum length 3' 19 | }) 20 | } 21 | 22 | const saltRounds = 10 23 | const passwordHash = await bcrypt.hash(password, saltRounds) 24 | 25 | const user = new User({ 26 | username, 27 | name, 28 | passwordHash, 29 | }) 30 | 31 | const savedUser = await user.save() 32 | 33 | response.json(savedUser) 34 | } catch (exception) { 35 | next(exception) 36 | } 37 | }) 38 | 39 | module.exports = usersRouter -------------------------------------------------------------------------------- /part_5/bloglist-backend/create_user.REST: -------------------------------------------------------------------------------- 1 | POST http://localhost:3001/api/users 2 | Content-Type: application/json 3 | 4 | { 5 | "username": "moi", 6 | "password": "moi", 7 | "name": "moi" 8 | } -------------------------------------------------------------------------------- /part_5/bloglist-backend/index.js: -------------------------------------------------------------------------------- 1 | const config = require('./utils/config') 2 | const http = require('http') 3 | 4 | const app = require('./app') 5 | 6 | const server = http.createServer(app) 7 | 8 | server.listen(config.PORT, () => { 9 | console.log(`Server running on port ${config.PORT}`) 10 | }) -------------------------------------------------------------------------------- /part_5/bloglist-backend/models/blog.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | 3 | const blogSchema = mongoose.Schema({ 4 | title: String, 5 | author: String, 6 | url: String, 7 | likes: Number, 8 | user: { 9 | type: mongoose.Schema.Types.ObjectId, 10 | ref: 'BlogUser' 11 | } 12 | }) 13 | 14 | blogSchema.set('toJSON', { 15 | transform: (document, returnedObject) => { 16 | returnedObject.id = returnedObject._id.toString() 17 | delete returnedObject._id 18 | delete returnedObject.__v 19 | } 20 | }) 21 | 22 | const Blog = mongoose.model('Blog', blogSchema) 23 | 24 | module.exports = Blog -------------------------------------------------------------------------------- /part_5/bloglist-backend/models/user.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const uniqueValidator = require("mongoose-unique-validator"); 3 | 4 | const userSchema = mongoose.Schema({ 5 | username: { 6 | type: String, 7 | unique: true, 8 | minlength: 3, 9 | present: true 10 | }, 11 | name: String, 12 | passwordHash: String, 13 | blogs: [ 14 | { 15 | type: mongoose.Schema.Types.ObjectId, 16 | ref: "Blog" 17 | } 18 | ] 19 | }); 20 | 21 | userSchema.plugin(uniqueValidator); 22 | 23 | userSchema.set("toJSON", { 24 | transform: (document, returnedObject) => { 25 | returnedObject.id = returnedObject._id.toString(); 26 | delete returnedObject.__v; 27 | delete returnedObject.passwordHash; 28 | } 29 | }); 30 | 31 | const User = mongoose.model("BlogUser", userSchema); 32 | 33 | module.exports = User; 34 | -------------------------------------------------------------------------------- /part_5/bloglist-backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "blogilista", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "cross-env NODE_ENV=production node index.js", 8 | "watch": "cross-env NODE_ENV=development nodemon index.js", 9 | "test": "cross-env NODE_ENV=test jest --verbose --runInBand", 10 | "lint": "eslint ." 11 | }, 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "bcrypt": "^3.0.6", 16 | "cors": "^2.8.5", 17 | "dotenv": "^8.0.0", 18 | "express": "^4.17.1", 19 | "jsonwebtoken": "^8.5.1", 20 | "mongoose": "^5.5.15", 21 | "mongoose-unique-validator": "^2.0.3" 22 | }, 23 | "devDependencies": { 24 | "cross-env": "^5.2.0", 25 | "eslint": "^5.16.0", 26 | "jest": "^24.8.0", 27 | "nodemon": "^1.19.1", 28 | "supertest": "^4.0.2" 29 | }, 30 | "jest": { 31 | "testEnvironment": "node" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /part_5/bloglist-backend/tests/test_helper.js: -------------------------------------------------------------------------------- 1 | const Blog = require('../models/blog') 2 | const User = require('../models/user') 3 | 4 | const initialBlogs = [ 5 | { 6 | title: "React patterns", 7 | author: "Michael Chan", 8 | url: "https://reactpatterns.com/", 9 | likes: 7, 10 | }, 11 | { 12 | title: "Go To Statement Considered Harmful", 13 | author: "Edsger W. Dijkstra", 14 | url: "http://www.u.arizona.edu/~rubinson/copyright_violations/Go_To_Considered_Harmful.html", 15 | likes: 5, 16 | }, 17 | ] 18 | 19 | const blogsInDb = async () => { 20 | const blogs = await Blog.find({}) 21 | return blogs.map(b => b.toJSON()) 22 | } 23 | 24 | const usersInDb = async () => { 25 | const users = await User.find({}) 26 | return users.map(u => u.toJSON()) 27 | } 28 | 29 | const equalTo = (blog) => (b) => 30 | b.author === blog.author && b.title === blog.title && b.url === blog.url 31 | 32 | module.exports = { 33 | initialBlogs, 34 | blogsInDb, 35 | equalTo, 36 | usersInDb 37 | } -------------------------------------------------------------------------------- /part_5/bloglist-backend/utils/config.js: -------------------------------------------------------------------------------- 1 | if (process.env.NODE_ENV !== 'production') { 2 | require('dotenv').config() 3 | } 4 | 5 | let PORT = process.env.PORT 6 | let MONGODB_URI = process.env.MONGODB_URI 7 | 8 | if (process.env.NODE_ENV === 'test') { 9 | PORT = process.env.TEST_PORT 10 | MONGODB_URI = process.env.TEST_MONGODB_URI 11 | } 12 | 13 | module.exports = { 14 | MONGODB_URI, 15 | PORT 16 | } -------------------------------------------------------------------------------- /part_5/bloglist-backend/utils/helper.js: -------------------------------------------------------------------------------- 1 | const config = require('./config') 2 | const User = require('../models/user') 3 | const mongoose = require('mongoose') 4 | 5 | console.log('HELPER') 6 | 7 | mongoose.connect(config.MONGODB_URI, { useNewUrlParser: true }) 8 | .then(() => { 9 | User.findByIdAndDelete('5c4857c4003ad1a6e6626932').then(resp => { 10 | console.log(resp) 11 | mongoose.connection.close() 12 | }) 13 | }) 14 | .catch((error) => { 15 | console.log('error connection to MongoDB:', error.message) 16 | }) 17 | -------------------------------------------------------------------------------- /part_5/bloglist-backend/utils/list_helper.js: -------------------------------------------------------------------------------- 1 | const dummy = (blogs) => 1 2 | 3 | const byLikes = (a, b) => b.likes - a.likes 4 | const byValue = (a, b) => b.value - a.value 5 | 6 | const authorWithGreatest = (counts) => { 7 | const authorValues = Object.keys(counts).map(author => ({ 8 | author, 9 | value: counts[author] 10 | })) 11 | 12 | return authorValues.sort(byValue)[0] 13 | } 14 | 15 | const totalLikes = (blogs) => { 16 | if (blogs.length === 0 ) { 17 | return 0 18 | } 19 | 20 | return blogs.reduce((s, b) => s + b.likes ,0) 21 | } 22 | 23 | const favoriteBlog = (blogs) => { 24 | if ( blogs.length===0 ) { 25 | return null 26 | } 27 | 28 | const { title, author, likes } = blogs.sort(byLikes)[0] 29 | 30 | return { title, author, likes } 31 | } 32 | 33 | const mostBlogs = (blogs) => { 34 | if (blogs.length === 0) { 35 | return null 36 | } 37 | 38 | const blogCount = blogs.reduce((obj, blog) => { 39 | if (obj[blog.author] === undefined) { 40 | obj[blog.author] = 0 41 | } 42 | 43 | obj[blog.author] += 1 44 | 45 | return obj 46 | }, {}) 47 | 48 | const { author, value } = authorWithGreatest(blogCount) 49 | 50 | return { 51 | author, 52 | blogs: value 53 | } 54 | } 55 | 56 | const mostLikes = (blogs) => { 57 | if (blogs.length === 0) { 58 | return null 59 | } 60 | 61 | const likeCount = blogs.reduce((obj, blog) => { 62 | if (obj[blog.author] === undefined ) { 63 | obj[blog.author] = 0 64 | } 65 | obj[blog.author] += blog.likes 66 | 67 | return obj 68 | }, {}) 69 | 70 | const { author, value } = authorWithGreatest(likeCount) 71 | 72 | return { 73 | author, 74 | likes: value 75 | } 76 | } 77 | 78 | module.exports = { 79 | dummy, 80 | totalLikes, 81 | favoriteBlog, 82 | mostBlogs, 83 | mostLikes 84 | } -------------------------------------------------------------------------------- /part_5/bloglist-backend/utils/middleware.js: -------------------------------------------------------------------------------- 1 | const getTokenFrom = request => { 2 | const authorization = request.get('authorization') 3 | if (authorization && authorization.toLowerCase().startsWith('bearer ')) { 4 | return authorization.substring(7) 5 | } 6 | return null 7 | } 8 | 9 | const tokenExtractor = (request, response, next) => { 10 | request.token = getTokenFrom(request) 11 | next() 12 | } 13 | 14 | const errorHandler = (error, request, response, next) => { 15 | if (error.name === 'CastError' && error.kind === 'ObjectId') { 16 | return response.status(400).send({ error: 'malformatted id' }) 17 | } else if (error.name === 'ValidationError') { 18 | return response.status(400).json({ error: error.message }) 19 | } else if (error.name === 'JsonWebTokenError') { 20 | return response.status(401).json({ error: 'invalid token' }) 21 | } 22 | 23 | console.error(error.message) 24 | 25 | next(error) 26 | } 27 | 28 | module.exports = { 29 | errorHandler, 30 | tokenExtractor 31 | } -------------------------------------------------------------------------------- /part_5/bloglist-frontend/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es6: true, 5 | 'jest/globals': true 6 | }, 7 | extends: ['eslint:recommended', 'plugin:react/recommended'], 8 | parserOptions: { 9 | ecmaFeatures: { 10 | jsx: true 11 | }, 12 | ecmaVersion: 2018, 13 | sourceType: 'module' 14 | }, 15 | plugins: ['react', 'jest'], 16 | rules: { 17 | indent: ['error', 2], 18 | 'linebreak-style': ['error', 'unix'], 19 | quotes: ['error', 'single'], 20 | semi: ['error', 'never'], 21 | eqeqeq: 'error', 22 | 'no-trailing-spaces': 'error', 23 | 'object-curly-spacing': ['error', 'always'], 24 | 'arrow-spacing': ['error', { before: true, after: true }], 25 | 'no-console': 0, 26 | 'react/prop-types': 0 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /part_5/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 | -------------------------------------------------------------------------------- /part_5/bloglist-frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bloglist-frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/react": "^8.0.1", 7 | "axios": "^0.19.0", 8 | "cross-env": "^5.2.0", 9 | "prop-types": "^15.7.2", 10 | "react": "^16.8.6", 11 | "react-dom": "^16.8.6", 12 | "react-scripts": "3.0.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 | "lint": "eslint ." 20 | }, 21 | "proxy": "http://localhost:3001", 22 | "eslintConfig": { 23 | "extends": "react-app" 24 | }, 25 | "browserslist": [ 26 | ">0.2%", 27 | "not dead", 28 | "not ie <= 11", 29 | "not op_mini all" 30 | ], 31 | "devDependencies": { 32 | "eslint-plugin-jest": "^22.6.4", 33 | "jest-dom": "^3.5.0", 34 | "react-testing-library": "^8.0.1" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /part_5/bloglist-frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/villeheikkila/fullstackopen/7d62b2885996e6e68a96fb4cf0133fb371095767/part_5/bloglist-frontend/public/favicon.ico -------------------------------------------------------------------------------- /part_5/bloglist-frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 15 | 16 | 25 | React App 26 | 27 | 28 | 29 |
    30 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /part_5/bloglist-frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /part_5/bloglist-frontend/src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render, waitForElement } from '@testing-library/react' 3 | jest.mock('./services/blogs') 4 | import App from './App' 5 | 6 | describe('', () => { 7 | test('if no user logged, blogs are not rendered', async () => { 8 | const component = render() 9 | 10 | await waitForElement(() => component.container.querySelector('.login')) 11 | 12 | const componentBlog = await component.container.querySelectorAll('.all') 13 | expect(componentBlog.length).toEqual(0) 14 | }) 15 | }) 16 | 17 | describe('', () => { 18 | beforeEach(() => { 19 | const user = { 20 | username: 'tester', 21 | token: '1231231214', 22 | name: 'Donald Tester' 23 | } 24 | 25 | localStorage.setItem('loggedBlogAppUser', JSON.stringify(user)) 26 | }) 27 | 28 | test('if user is logged in, blogs are rendered', async () => { 29 | const component = render() 30 | await waitForElement(() => component.container.querySelector('.all')) 31 | 32 | const componentBlog = await component.container.querySelectorAll('.all') 33 | expect(componentBlog.length).toEqual(2) 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /part_5/bloglist-frontend/src/components/Blog.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import blogService from '../services/blogs' 3 | 4 | const blogStyle = { 5 | paddingTop: 10, 6 | paddingLeft: 2, 7 | border: 'solid', 8 | borderWidth: 1, 9 | marginBottom: 5 10 | } 11 | 12 | const Blog = ({ blog, setUpdate, user }) => { 13 | const [visible, setVisible] = useState(false) 14 | const [removeVisible, setRemoveVisible] = useState(false) 15 | 16 | const hideWhenVisible = { display: visible ? 'none' : '' } 17 | const showWhenVisible = { display: visible ? '' : 'none' } 18 | 19 | const hideWhenNotOwned = { display: removeVisible ? 'none' : '' } 20 | 21 | const rules = () => { 22 | setVisible(true) 23 | if (blog.user.username !== user.username) { 24 | setRemoveVisible(true) 25 | } 26 | } 27 | 28 | const like = async event => { 29 | event.preventDefault() 30 | const likes = blog.likes + 1 31 | const newBlog = { ...blog, likes } 32 | await blogService.update(blog.id, newBlog) 33 | setUpdate(Math.floor(Math.random() * 100)) 34 | } 35 | 36 | const remove = async event => { 37 | event.preventDefault() 38 | 39 | if (window.confirm(`remove blog ${blog.title}) by ${blog.author}`)) { 40 | blogService.setToken(user.token) 41 | await blogService.remove(blog.id, user.token) 42 | setUpdate(Math.floor(Math.random() * 100)) 43 | } 44 | } 45 | 46 | return ( 47 |
    48 |
    49 |
    50 | {blog.title} {blog.author} 51 |
    52 |
    53 |
    54 | {blog.title}
    55 | {blog.url}
    56 |
    57 | {blog.likes} likes 58 | 59 |
    60 |
    61 | added by {blog.author} 62 | 63 |
    64 |
    65 |
    66 | ) 67 | } 68 | 69 | export default Blog 70 | -------------------------------------------------------------------------------- /part_5/bloglist-frontend/src/components/Blog.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import 'jest-dom/extend-expect' 3 | import { render, cleanup, fireEvent } from '@testing-library/react' 4 | import Blog from './Blog' 5 | 6 | afterEach(cleanup) 7 | describe.only('', () => { 8 | let component 9 | let mockHandler 10 | 11 | beforeEach(() => { 12 | const blog = { 13 | title: 'titteli', 14 | author: 'authori', 15 | url: 'urli', 16 | user: '12345678910', 17 | likes: 1 18 | } 19 | const user = { 20 | username: 'moi' 21 | } 22 | 23 | let setUpdate 24 | 25 | mockHandler = jest.fn() 26 | component = render( 27 | 33 | ) 34 | }) 35 | 36 | it('renders its title', () => { 37 | const div = component.container.querySelector('.titleauthor') 38 | 39 | expect(div).toHaveTextContent('titteli') 40 | }) 41 | 42 | it('renders its author', () => { 43 | const div = component.container.querySelector('.titleauthor') 44 | 45 | expect(div).toHaveTextContent('authori') 46 | }) 47 | 48 | it('click shows more', () => { 49 | const button = component.getByText('titteli authori') 50 | fireEvent.click(button) 51 | const div = component.container.querySelector('.titleauthorlikedelete') 52 | 53 | expect(div).toHaveTextContent( 54 | 'titteli urli 1 likeslikeadded by authoriremove' 55 | ) 56 | }) 57 | }) 58 | -------------------------------------------------------------------------------- /part_5/bloglist-frontend/src/components/Notification.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Notification = ({ message }) => { 4 | if (message === null) { 5 | return null 6 | } 7 | 8 | return
    {message}
    9 | } 10 | 11 | export default Notification 12 | -------------------------------------------------------------------------------- /part_5/bloglist-frontend/src/components/SimpleBlog.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const SimpleBlog = ({ blog, onClick }) => ( 4 |
    5 |
    6 | {blog.title} {blog.author} 7 |
    8 |
    9 | blog has {blog.likes} likes 10 | 13 |
    14 |
    15 | ) 16 | 17 | export default SimpleBlog 18 | -------------------------------------------------------------------------------- /part_5/bloglist-frontend/src/components/SimpleBlog.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import 'jest-dom/extend-expect' 3 | import { render, cleanup, fireEvent } from 'react-testing-library' 4 | import SimpleBlog from './SimpleBlog' 5 | 6 | afterEach(cleanup) 7 | describe.only('', () => { 8 | let component 9 | let mockHandler 10 | 11 | beforeEach(() => { 12 | const blog = { 13 | title: 'titteli', 14 | author: 'authori', 15 | url: 'urli', 16 | user: '12345678910', 17 | likes: 1 18 | } 19 | mockHandler = jest.fn() 20 | component = render() 21 | }) 22 | 23 | it('renders its title', () => { 24 | const div = component.container.querySelector('.titleauthor') 25 | 26 | expect(div).toHaveTextContent('titteli') 27 | }) 28 | 29 | it('renders its author', () => { 30 | const div = component.container.querySelector('.titleauthor') 31 | 32 | expect(div).toHaveTextContent('authori') 33 | }) 34 | 35 | it('renders its likes', () => { 36 | const div = component.container.querySelector('.likes') 37 | 38 | expect(div).toHaveTextContent(1) 39 | }) 40 | 41 | it('like clicked twice works ', () => { 42 | const button = component.getByText('like') 43 | fireEvent.click(button) 44 | fireEvent.click(button) 45 | 46 | expect(mockHandler.mock.calls.length).toBe(2) 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /part_5/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.propTypes = { 34 | buttonLabel: PropTypes.string.isRequired 35 | } 36 | 37 | export default Togglable 38 | -------------------------------------------------------------------------------- /part_5/bloglist-frontend/src/hooks/index.js: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | 3 | 4 | export const useField = (type) => { 5 | const [value, setValue] = useState('') 6 | const onChange = (event) => { 7 | setValue(event.target.value) 8 | } 9 | 10 | const reset = () => { 11 | setValue('') 12 | } 13 | const olio = { 14 | type: type, 15 | value: value, 16 | onChange: onChange 17 | } 18 | return { 19 | olio, 20 | reset 21 | } 22 | } -------------------------------------------------------------------------------- /part_5/bloglist-frontend/src/index.css: -------------------------------------------------------------------------------- 1 | .error { 2 | color: red; 3 | background: lightgrey; 4 | font-size: 20px; 5 | border-style: solid; 6 | border-radius: 5px; 7 | padding: 10px; 8 | margin-bottom: 10px; 9 | } 10 | -------------------------------------------------------------------------------- /part_5/bloglist-frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import App from './App' 4 | import './index.css' 5 | 6 | ReactDOM.render(, document.getElementById('root')) -------------------------------------------------------------------------------- /part_5/bloglist-frontend/src/services/__mocks__/blogs.js: -------------------------------------------------------------------------------- 1 | const blogs = [ 2 | { 3 | _id: '5c745bc4cd1fb549d840d685', 4 | title: 'adsda', 5 | author: 'dasdasda', 6 | url: '1111111111111', 7 | user: { _id: '5c73dd9ccd1fb549d840d64b', username: 'hii', name: 'haa' }, 8 | likes: 1 9 | }, 10 | { 11 | _id: '5c73e82bcd1fb549d840d64c', 12 | title: 'titteli', 13 | author: 'authori', 14 | url: 'urli', 15 | user: { _id: '5c73dd9ccd1fb549d840d64b', username: 'saa', name: 'sii' }, 16 | likes: 2 17 | } 18 | ] 19 | 20 | const getAll = () => { 21 | return Promise.resolve(blogs) 22 | } 23 | 24 | export default { getAll } 25 | -------------------------------------------------------------------------------- /part_5/bloglist-frontend/src/services/blogs.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | const baseUrl = '/api/blogs' 3 | 4 | let token = null 5 | 6 | const setToken = newToken => { 7 | token = `bearer ${newToken}` 8 | } 9 | 10 | const config = { headers: { Authorization: token } } 11 | 12 | const getAll = () => { 13 | const request = axios.get(baseUrl) 14 | return request.then(response => response.data) 15 | } 16 | 17 | const create = async (newObject, auth) => { 18 | setToken(auth) 19 | const config = { headers: { Authorization: token } } 20 | 21 | const response = await axios.post(baseUrl, newObject, config) 22 | return response.data 23 | } 24 | 25 | const update = (id, newObject) => { 26 | const request = axios.put(`${baseUrl}/${id}`, newObject) 27 | return request.then(response => response.data) 28 | } 29 | 30 | const remove = (id, auth) => { 31 | setToken(auth) 32 | const authoriz = { headers: { Authorization: token } } 33 | 34 | console.log('auht', auth) 35 | console.log('config', config) 36 | 37 | const request = axios.delete(`${baseUrl}/${id}`, authoriz) 38 | return request.then(response => response.data) 39 | } 40 | 41 | export default { getAll, create, update, setToken, remove } 42 | -------------------------------------------------------------------------------- /part_5/bloglist-frontend/src/services/login.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | const baseUrl = '/api/login' 3 | 4 | const login = async credentials => { 5 | const response = await axios.post(baseUrl, credentials) 6 | return response.data 7 | } 8 | 9 | export default { login } 10 | -------------------------------------------------------------------------------- /part_5/bloglist-frontend/src/setupTests.js: -------------------------------------------------------------------------------- 1 | let savedItems = {} 2 | 3 | const localStorageMock = { 4 | setItem: (key, item) => { 5 | savedItems[key] = item 6 | }, 7 | getItem: key => savedItems[key], 8 | clear: (savedItems = {}) 9 | } 10 | 11 | window.localStorage = localStorageMock 12 | -------------------------------------------------------------------------------- /part_5/custom-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 | -------------------------------------------------------------------------------- /part_5/custom-hooks/README.md: -------------------------------------------------------------------------------- 1 | ## usage 2 | 3 | Run fronend in development mode with _npm seerver_ 4 | 5 | Start server to port 3005 with _npm run server_ -------------------------------------------------------------------------------- /part_5/custom-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 | "id": 3 13 | } 14 | ], 15 | "persons": [ 16 | { 17 | "name": "mluukkai", 18 | "number": "040-5483923", 19 | "id": 1 20 | }, 21 | { 22 | "id": 2 23 | }, 24 | { 25 | "id": 3 26 | }, 27 | { 28 | "id": 4 29 | }, 30 | { 31 | "id": 5 32 | } 33 | ] 34 | } -------------------------------------------------------------------------------- /part_5/custom-hooks/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hooks", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "axios": "^0.19.0", 7 | "json-server": "^0.15.0", 8 | "react": "^16.8.6", 9 | "react-dom": "^16.8.6", 10 | "react-scripts": "3.0.1" 11 | }, 12 | "scripts": { 13 | "start": "react-scripts start", 14 | "build": "react-scripts build", 15 | "test": "react-scripts test", 16 | "eject": "react-scripts eject", 17 | "server": "json-server -p 3005 db.json" 18 | }, 19 | "eslintConfig": { 20 | "extends": "react-app" 21 | }, 22 | "browserslist": [ 23 | ">0.2%", 24 | "not dead", 25 | "not ie <= 11", 26 | "not op_mini all" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /part_5/custom-hooks/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/villeheikkila/fullstackopen/7d62b2885996e6e68a96fb4cf0133fb371095767/part_5/custom-hooks/public/favicon.ico -------------------------------------------------------------------------------- /part_5/custom-hooks/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 15 | 16 | 25 | React App 26 | 27 | 28 | 29 |
    30 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /part_5/custom-hooks/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /part_5/custom-hooks/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import axios from "axios"; 3 | 4 | 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 | const useResource = baseUrl => { 19 | const [resources, setResources] = useState([]); 20 | 21 | if (resources.length === 0) { 22 | axios.get(baseUrl).then(response => setResources(response.data)); 23 | } 24 | 25 | const create = async resource => { 26 | await axios.post(baseUrl); 27 | const newResources = resources.concat(resource); 28 | setResources(newResources); 29 | }; 30 | 31 | const service = { 32 | create 33 | }; 34 | 35 | return [resources, service]; 36 | }; 37 | 38 | const App = () => { 39 | const content = useField("text"); 40 | const name = useField("text"); 41 | const number = useField("text"); 42 | 43 | const [notes, noteService] = useResource("http://localhost:3005/notes"); 44 | const [persons, personService] = useResource("http://localhost:3005/persons"); 45 | 46 | const handleNoteSubmit = event => { 47 | event.preventDefault(); 48 | noteService.create({ content: content.value }); 49 | }; 50 | 51 | const handlePersonSubmit = event => { 52 | event.preventDefault(); 53 | personService.create({ name: name.value, number: number.value }); 54 | }; 55 | 56 | return ( 57 |
    58 |

    notes

    59 |
    60 | 61 | 62 |
    63 | {notes.map(n => ( 64 |

    {n.content}

    65 | ))} 66 | 67 |

    persons

    68 |
    69 | name
    70 | number 71 | 72 |
    73 | {persons.map(n => ( 74 |

    75 | {n.name} {n.number} 76 |

    77 | ))} 78 |
    79 | ); 80 | }; 81 | 82 | export default App; 83 | -------------------------------------------------------------------------------- /part_5/custom-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 | -------------------------------------------------------------------------------- /part_6/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 | -------------------------------------------------------------------------------- /part_6/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": 4 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": 16 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": 3 17 | }, 18 | { 19 | "content": "Premature optimization is the root of all evil.", 20 | "id": "25170", 21 | "votes": 5 22 | }, 23 | { 24 | "content": "", 25 | "id": "77766", 26 | "votes": 0 27 | }, 28 | { 29 | "content": "asdasdasd", 30 | "id": "66599", 31 | "votes": 2 32 | }, 33 | { 34 | "content": "ddddd", 35 | "id": "78991", 36 | "votes": 1 37 | }, 38 | { 39 | "content": "sadxasdsa", 40 | "id": "8224", 41 | "votes": 4 42 | }, 43 | { 44 | "content": "adssa", 45 | "id": "13070", 46 | "votes": 1 47 | }, 48 | { 49 | "content": "dasdasd", 50 | "id": "67398", 51 | "votes": 0 52 | }, 53 | { 54 | "content": "asdasd", 55 | "id": "80315", 56 | "votes": 0 57 | }, 58 | { 59 | "content": "asdasdsad", 60 | "id": "23422", 61 | "votes": 0 62 | } 63 | ] 64 | } -------------------------------------------------------------------------------- /part_6/redux-anecdotes/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-anecdotes", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "axios": "^0.19.0", 7 | "json-server": "^0.15.0", 8 | "react": "^16.8.6", 9 | "react-dom": "^16.8.6", 10 | "react-redux": "^7.1.0", 11 | "react-scripts": "3.0.1", 12 | "redux": "^4.0.1", 13 | "redux-thunk": "^2.3.0" 14 | }, 15 | "scripts": { 16 | "start": "react-scripts start", 17 | "build": "react-scripts build", 18 | "test": "react-scripts test", 19 | "eject": "react-scripts eject", 20 | "server": "json-server -p3001 db.json" 21 | }, 22 | "eslintConfig": { 23 | "extends": "react-app" 24 | }, 25 | "browserslist": [ 26 | ">0.2%", 27 | "not dead", 28 | "not ie <= 11", 29 | "not op_mini all" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /part_6/redux-anecdotes/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/villeheikkila/fullstackopen/7d62b2885996e6e68a96fb4cf0133fb371095767/part_6/redux-anecdotes/public/favicon.ico -------------------------------------------------------------------------------- /part_6/redux-anecdotes/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 15 | 16 | 25 | React App 26 | 27 | 28 | 29 |
    30 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /part_6/redux-anecdotes/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /part_6/redux-anecdotes/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react' 2 | import { connect } from 'react-redux' 3 | import Notification from './components/Notification' 4 | import AnecdoteForm from './components/AnecdoteForm' 5 | import AnecdoteList from './components/AnecdoteList' 6 | import Filter from './components/Filter' 7 | import { initializeAnecdotes } from './reducers/anecdoteReducer' 8 | 9 | 10 | const App = (props) => { 11 | useEffect(() => { 12 | props.initializeAnecdotes() 13 | }, [props]) 14 | 15 | return ( 16 |
    17 |

    Anecdotes

    18 | 19 | 20 | 21 | 22 |
    23 | ) 24 | } 25 | 26 | export default connect(null, { initializeAnecdotes })(App) -------------------------------------------------------------------------------- /part_6/redux-anecdotes/src/components/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/villeheikkila/fullstackopen/7d62b2885996e6e68a96fb4cf0133fb371095767/part_6/redux-anecdotes/src/components/.keep -------------------------------------------------------------------------------- /part_6/redux-anecdotes/src/components/AnecdoteForm.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createAnecdote } from '../reducers/anecdoteReducer' 3 | import { createNotification, deleteNotification } from '../reducers/notificationReducer' 4 | import { connect } from 'react-redux' 5 | 6 | const AnecdoteForm = (props) => { 7 | const addAnecdote = async (event) => { 8 | event.preventDefault() 9 | const content = event.target.anecdote.value 10 | props.createNotification(`you added anecdote '${content}'`, 3) 11 | event.target.anecdote.value = '' 12 | props.createAnecdote(content) 13 | } 14 | 15 | return ( 16 |
    17 |
    18 |
    19 | 20 |
    21 |
    22 | ) 23 | } 24 | 25 | const mapStateToProps = (state) => { 26 | return { 27 | filter: state.filter, 28 | } 29 | } 30 | 31 | const mapDispatchToProps = { createAnecdote, createNotification, deleteNotification } 32 | 33 | const ConnectedAnecdoteForm = connect(mapStateToProps, mapDispatchToProps)(AnecdoteForm) 34 | export default ConnectedAnecdoteForm 35 | -------------------------------------------------------------------------------- /part_6/redux-anecdotes/src/components/AnecdoteList.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { vote } from '../reducers/anecdoteReducer' 3 | import { createNotification, deleteNotification } from '../reducers/notificationReducer' 4 | import { connect } from 'react-redux' 5 | 6 | 7 | const AnecdoteList = (props) => { 8 | const voteId = (content, id) => { 9 | props.vote(id) 10 | props.createNotification(`you voted '${content}'`, 3) 11 | } 12 | 13 | return ( 14 |
    15 | {props.visibleAnecdotes.map(anecdote => 16 |
    17 |
    18 | {anecdote.content} 19 |
    20 |
    21 | has {anecdote.votes} 22 | 23 |
    24 |
    25 | )} 26 |
    27 | ) 28 | } 29 | 30 | const anecdotesToShow = ({ anecdotes, filter }) => { 31 | const anecdotesSorted = [...anecdotes].sort((a, b) => { 32 | return b.votes - a.votes 33 | }) 34 | 35 | if (filter === '') { 36 | return anecdotesSorted 37 | } else { 38 | return anecdotesSorted.filter(e => e.content.toLowerCase().includes(filter.toLowerCase())) 39 | } 40 | } 41 | 42 | const mapStateToProps = (state) => { 43 | return { 44 | visibleAnecdotes: anecdotesToShow(state), 45 | } 46 | } 47 | 48 | const mapDispatchToProps = { vote, createNotification, deleteNotification } 49 | 50 | const ConnectedAnecdoteList = connect(mapStateToProps, mapDispatchToProps)(AnecdoteList) 51 | export default ConnectedAnecdoteList 52 | -------------------------------------------------------------------------------- /part_6/redux-anecdotes/src/components/Filter.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { setFilter } from '../reducers/filterReducer' 3 | import { connect } from 'react-redux' 4 | 5 | const Filter = (props) => { 6 | const handleChange = (event) => { 7 | props.setFilter(event.target.value) 8 | } 9 | 10 | const style = { 11 | marginBottom: 10 12 | } 13 | 14 | return ( 15 |
    16 | filter 17 |
    18 | ) 19 | } 20 | 21 | 22 | const mapStateToProps = (state) => { 23 | return { 24 | filter: state.filter, 25 | } 26 | } 27 | 28 | const mapDispatchToProps = { setFilter } 29 | 30 | const ConnectedFilter = connect(mapStateToProps, mapDispatchToProps)(Filter) 31 | export default ConnectedFilter -------------------------------------------------------------------------------- /part_6/redux-anecdotes/src/components/Notification.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux' 3 | 4 | const Notification = (props) => { 5 | const style = { 6 | border: 'solid', 7 | padding: 10, 8 | borderWidth: 1 9 | } 10 | 11 | if (props.notification === "") { 12 | return null 13 | } 14 | 15 | return ( 16 |
    17 | {props.notification} 18 |
    19 | ) 20 | } 21 | const mapStateToProps = (state) => { 22 | return { 23 | notification: state.notification, 24 | } 25 | } 26 | 27 | const ConnectedNotification = connect(mapStateToProps)(Notification) 28 | export default ConnectedNotification 29 | -------------------------------------------------------------------------------- /part_6/redux-anecdotes/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import App from './App' 4 | import { Provider } from 'react-redux' 5 | import store from './store' 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root') 12 | ) 13 | -------------------------------------------------------------------------------- /part_6/redux-anecdotes/src/reducers/anecdoteReducer.js: -------------------------------------------------------------------------------- 1 | import anecdotesService from '../services/anecdotes' 2 | 3 | const getId = () => (100000 * Math.random()).toFixed(0) 4 | 5 | const asObject = (anecdote) => { 6 | return { 7 | content: anecdote, 8 | id: getId(), 9 | votes: 0 10 | } 11 | } 12 | 13 | export const createAnecdote = (content) => { 14 | return async dispatch => { 15 | const newAnecdote = await anecdotesService.create(asObject(content)) 16 | dispatch({ 17 | type: 'NEW_ANECDOTE', 18 | data: newAnecdote 19 | }) 20 | } 21 | } 22 | 23 | export const vote = (id) => { 24 | return async dispatch => { 25 | const updatedAnecdote = await anecdotesService.incrementLikes(id) 26 | dispatch({ 27 | type: 'VOTE', 28 | data: updatedAnecdote 29 | }) 30 | } 31 | } 32 | 33 | export const initializeAnecdotes = () => { 34 | return async dispatch => { 35 | const anecdotes = await anecdotesService.getAll() 36 | dispatch({ 37 | type: 'INIT_ANECDOTES', 38 | data: anecdotes, 39 | }) 40 | } 41 | } 42 | 43 | const anecdoteReducer = (state = [], action) => { 44 | switch (action.type) { 45 | case 'VOTE': 46 | const id = action.data.id 47 | return state.map(anecdote => 48 | anecdote.id !== id ? anecdote : action.data 49 | ) 50 | case 'NEW_ANECDOTE': 51 | return [...state, action.data] 52 | case 'INIT_ANECDOTES': 53 | console.log('action.data: ', action.data); 54 | 55 | return action.data 56 | default: 57 | return state 58 | } 59 | } 60 | 61 | export default anecdoteReducer -------------------------------------------------------------------------------- /part_6/redux-anecdotes/src/reducers/filterReducer.js: -------------------------------------------------------------------------------- 1 | export const setFilter = (filter) => { 2 | return { 3 | type: 'SET_FILTER', 4 | data: filter 5 | } 6 | } 7 | 8 | const filterReducer = (state = "", action) => { 9 | switch (action.type) { 10 | case 'SET_FILTER': 11 | return action.data 12 | default: 13 | return state 14 | } 15 | } 16 | 17 | export default filterReducer -------------------------------------------------------------------------------- /part_6/redux-anecdotes/src/reducers/notificationReducer.js: -------------------------------------------------------------------------------- 1 | export const createNotification = (content, time) => { 2 | return async dispatch => { 3 | dispatch({ 4 | type: 'SET_NOTIFICATION', 5 | data: content 6 | }) 7 | setTimeout(() => { 8 | dispatch({ 9 | type: 'DELETE_NOTIFICATION', 10 | }) 11 | }, time * 1000) 12 | } 13 | } 14 | 15 | export const deleteNotification = () => { 16 | return { 17 | type: 'DELETE_NOTIFICATION', 18 | } 19 | } 20 | 21 | const notificationReducer = (state = "", action) => { 22 | switch (action.type) { 23 | case 'SET_NOTIFICATION': 24 | return action.data 25 | case 'DELETE_NOTIFICATION': 26 | return "" 27 | default: 28 | return state 29 | } 30 | } 31 | 32 | export default notificationReducer -------------------------------------------------------------------------------- /part_6/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 getOne = async (id) => { 11 | const response = await axios.get(`${baseUrl}/${id}/`) 12 | return response.data 13 | } 14 | 15 | 16 | const create = async (data) => { 17 | console.log('data: ', data); 18 | const response = await axios.post(baseUrl, data) 19 | return response.data 20 | } 21 | 22 | const incrementLikes = async (id) => { 23 | const old = await getOne(id) 24 | old.votes = old.votes + 1 25 | const response = await axios.put(`${baseUrl}/${id}/`, old) 26 | return response.data 27 | } 28 | 29 | export default { getAll, create, incrementLikes } -------------------------------------------------------------------------------- /part_6/redux-anecdotes/src/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, combineReducers, applyMiddleware } from 'redux' 2 | import thunk from 'redux-thunk'; 3 | 4 | import notificationReducer from './reducers/notificationReducer' 5 | import anecdoteReducer from './reducers/anecdoteReducer' 6 | import filterReducer from './reducers/filterReducer' 7 | 8 | const reducer = combineReducers({ 9 | notification: notificationReducer, 10 | anecdotes: anecdoteReducer, 11 | filter: filterReducer 12 | }) 13 | 14 | const store = createStore(reducer, applyMiddleware(thunk)) 15 | 16 | export default store -------------------------------------------------------------------------------- /part_6/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 | -------------------------------------------------------------------------------- /part_6/unicafe-redux/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "unicafe-redux", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "deep-freeze": "0.0.1", 7 | "react": "^16.8.6", 8 | "react-dom": "^16.8.6", 9 | "react-scripts": "3.0.1", 10 | "redux": "^4.0.1" 11 | }, 12 | "scripts": { 13 | "start": "react-scripts start", 14 | "build": "react-scripts build", 15 | "test": "react-scripts test", 16 | "eject": "react-scripts eject" 17 | }, 18 | "eslintConfig": { 19 | "extends": "react-app" 20 | }, 21 | "browserslist": [ 22 | ">0.2%", 23 | "not dead", 24 | "not ie <= 11", 25 | "not op_mini all" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /part_6/unicafe-redux/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/villeheikkila/fullstackopen/7d62b2885996e6e68a96fb4cf0133fb371095767/part_6/unicafe-redux/public/favicon.ico -------------------------------------------------------------------------------- /part_6/unicafe-redux/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 15 | 16 | 25 | React App 26 | 27 | 28 | 29 |
    30 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /part_6/unicafe-redux/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /part_6/unicafe-redux/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom' 3 | import { createStore } from 'redux' 4 | import reducer from './reducer' 5 | 6 | const store = createStore(reducer) 7 | 8 | const App = () => { 9 | const good = () => { 10 | store.dispatch({ 11 | type: 'GOOD' 12 | }) 13 | } 14 | const ok = () => { 15 | store.dispatch({ 16 | type: 'OK' 17 | }) 18 | } 19 | const bad = () => { 20 | store.dispatch({ 21 | type: 'BAD' 22 | }) 23 | } 24 | const zero = () => { 25 | store.dispatch({ 26 | type: 'ZERO' 27 | }) 28 | } 29 | 30 | return ( 31 |
    32 | 33 | 34 | 35 | 36 |
    hyvä {store.getState().good}
    37 |
    neutraali {store.getState().ok}
    38 |
    huono {store.getState().bad}
    39 |
    40 | ) 41 | } 42 | 43 | const renderApp = () => { 44 | console.log(store.getState().good) 45 | ReactDOM.render(, document.getElementById('root')) 46 | } 47 | 48 | renderApp() 49 | store.subscribe(renderApp) 50 | -------------------------------------------------------------------------------- /part_6/unicafe-redux/src/reducer.js: -------------------------------------------------------------------------------- 1 | const initialState = { 2 | good: 0, 3 | ok: 0, 4 | bad: 0 5 | } 6 | 7 | const counterReducer = (state = initialState, action) => { 8 | console.log(action) 9 | switch (action.type) { 10 | case 'GOOD': 11 | const newState = { 12 | good: state.good + 1, 13 | bad: state.bad, 14 | ok: state.ok 15 | } 16 | return newState 17 | case 'OK': 18 | const newStateOk = { 19 | good: state.good, 20 | bad: state.bad, 21 | ok: state.ok + 1 22 | } 23 | return newStateOk 24 | case 'BAD': 25 | const newStateBad = { 26 | good: state.good, 27 | bad: state.bad + 1, 28 | ok: state.ok 29 | } 30 | return newStateBad 31 | case 'ZERO': 32 | const newStateZero = { 33 | good: 0, 34 | bad: 0, 35 | ok: 0 36 | } 37 | return newStateZero 38 | default: return state 39 | } 40 | 41 | } 42 | 43 | export default counterReducer -------------------------------------------------------------------------------- /part_6/unicafe-redux/src/reducer.test.js: -------------------------------------------------------------------------------- 1 | import deepFreeze from 'deep-freeze' 2 | import counterReducer from './reducer' 3 | 4 | describe('unicafe reducer', () => { 5 | const initialState = { 6 | good: 0, 7 | ok: 0, 8 | bad: 0 9 | } 10 | 11 | test('should return a proper initial state when called with undefined state', () => { 12 | const state = {} 13 | const action = { 14 | type: 'DO_NOTHING' 15 | } 16 | 17 | const newState = counterReducer(undefined, action) 18 | expect(newState).toEqual(initialState) 19 | }) 20 | 21 | test('good is incremented', () => { 22 | const action = { 23 | type: 'GOOD' 24 | } 25 | const state = initialState 26 | 27 | deepFreeze(state) 28 | const newState = counterReducer(state, action) 29 | expect(newState).toEqual({ 30 | good: 1, 31 | ok: 0, 32 | bad: 0 33 | }) 34 | }) 35 | 36 | test('ok is incremented', () => { 37 | const action = { 38 | type: 'OK' 39 | } 40 | const state = initialState 41 | 42 | deepFreeze(state) 43 | const newState = counterReducer(state, action) 44 | expect(newState).toEqual({ 45 | good: 0, 46 | ok: 1, 47 | bad: 0 48 | }) 49 | }) 50 | 51 | test('bad is incremented', () => { 52 | const action = { 53 | type: 'BAD' 54 | } 55 | const state = initialState 56 | 57 | deepFreeze(state) 58 | const newState = counterReducer(state, action) 59 | expect(newState).toEqual({ 60 | good: 0, 61 | ok: 0, 62 | bad: 1 63 | }) 64 | }) 65 | 66 | test('bad is incremented', () => { 67 | const initState = { 68 | good: 5, 69 | ok: 3, 70 | bad: 2 71 | } 72 | 73 | const action = { 74 | type: 'ZERO' 75 | } 76 | const state = initState 77 | 78 | deepFreeze(state) 79 | const newState = counterReducer(state, action) 80 | expect(newState).toEqual({ 81 | good: 0, 82 | ok: 0, 83 | bad: 0 84 | }) 85 | }) 86 | 87 | 88 | }) -------------------------------------------------------------------------------- /part_7/bloglist-backend/app.js: -------------------------------------------------------------------------------- 1 | const config = require('./utils/config') 2 | const express = require('express') 3 | const app = express() 4 | const bodyParser = require('body-parser') 5 | const cors = require('cors') 6 | const mongoose = require('mongoose') 7 | 8 | const { tokenExtractor, errorHandler } = require('./utils/middleware') 9 | 10 | const loginRouter = require('./controllers/login') 11 | const blogsRouter = require('./controllers/blogs') 12 | const usersRouter = require('./controllers/users') 13 | 14 | app.use(cors()) 15 | app.use(bodyParser.json()) 16 | 17 | console.log('connecting to', config.MONGODB_URI) 18 | 19 | mongoose.connect(config.MONGODB_URI, { useNewUrlParser: true }) 20 | .then(() => { 21 | console.log('connected to MongoDB') 22 | }) 23 | .catch((error) => { 24 | console.log('error connection to MongoDB:', error.message) 25 | }) 26 | 27 | app.use(tokenExtractor) 28 | 29 | app.use('/api/login', loginRouter) 30 | app.use('/api/blogs', blogsRouter) 31 | app.use('/api/users', usersRouter) 32 | 33 | app.use(errorHandler) 34 | 35 | 36 | module.exports = app -------------------------------------------------------------------------------- /part_7/bloglist-backend/controllers/login.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken') 2 | const bcrypt = require('bcrypt') 3 | const loginRouter = require('express').Router() 4 | const User = require('../models/user') 5 | 6 | loginRouter.post('/', async (request, response) => { 7 | const body = request.body 8 | 9 | const user = await User.findOne({ username: body.username }) 10 | const passwordCorrect = 11 | user === null 12 | ? false 13 | : await bcrypt.compare(body.password, user.passwordHash) 14 | 15 | console.log(user) 16 | 17 | if (!(user && passwordCorrect)) { 18 | return response.status(401).json({ 19 | error: 'invalid username or password' 20 | }) 21 | } 22 | 23 | const userForToken = { 24 | username: user.username, 25 | id: user._id, 26 | } 27 | 28 | const token = jwt.sign(userForToken, process.env.SECRET) 29 | 30 | response 31 | .status(200) 32 | .send({ token, username: user.username, name: user.name }) 33 | }) 34 | 35 | module.exports = loginRouter -------------------------------------------------------------------------------- /part_7/bloglist-backend/controllers/users.js: -------------------------------------------------------------------------------- 1 | const bcrypt = require('bcrypt') 2 | const usersRouter = require('express').Router() 3 | const User = require('../models/user') 4 | 5 | const formatUser = input => { 6 | return { 7 | blogs: input.blogs, 8 | id: input.id, 9 | name: input.name, 10 | username: input.username, 11 | } 12 | } 13 | 14 | usersRouter.get('/', async (request, response) => { 15 | const users = await User 16 | .find({}).populate('blogs', { author: 1, title: 1, url: 1 }) 17 | 18 | response.json(users.map(formatUser)) 19 | }) 20 | 21 | usersRouter.post('/', async (request, response, next) => { 22 | try { 23 | const { username, password, name } = request.body 24 | 25 | if (!password || password.length < 3) { 26 | return response.status(400).send({ 27 | error: 'pasword minimum length 3' 28 | }) 29 | } 30 | 31 | const saltRounds = 10 32 | const passwordHash = await bcrypt.hash(password, saltRounds) 33 | 34 | const user = new User({ 35 | username, 36 | name, 37 | passwordHash, 38 | }) 39 | 40 | const savedUser = await user.save() 41 | 42 | response.json(savedUser) 43 | } catch (exception) { 44 | next(exception) 45 | } 46 | }) 47 | 48 | module.exports = usersRouter -------------------------------------------------------------------------------- /part_7/bloglist-backend/index.js: -------------------------------------------------------------------------------- 1 | const config = require('./utils/config') 2 | const http = require('http') 3 | 4 | const app = require('./app') 5 | 6 | const server = http.createServer(app) 7 | 8 | server.listen(config.PORT, () => { 9 | console.log(`Server running on port ${config.PORT}`) 10 | }) -------------------------------------------------------------------------------- /part_7/bloglist-backend/models/blog.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | 3 | const blogSchema = mongoose.Schema({ 4 | title: String, 5 | author: String, 6 | url: String, 7 | likes: Number, 8 | user: { 9 | type: mongoose.Schema.Types.ObjectId, 10 | ref: 'BlogUser' 11 | } 12 | }) 13 | 14 | blogSchema.set('toJSON', { 15 | transform: (document, returnedObject) => { 16 | returnedObject.id = returnedObject._id.toString() 17 | delete returnedObject._id 18 | delete returnedObject.__v 19 | } 20 | }) 21 | 22 | const Blog = mongoose.model('Blog', blogSchema) 23 | 24 | module.exports = Blog -------------------------------------------------------------------------------- /part_7/bloglist-backend/models/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node' 3 | }; -------------------------------------------------------------------------------- /part_7/bloglist-backend/models/user.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | const uniqueValidator = require('mongoose-unique-validator') 3 | 4 | const userSchema = mongoose.Schema({ 5 | username: { 6 | type: String, 7 | unique: true, 8 | minlength: 3, 9 | present: true 10 | }, 11 | name: String, 12 | passwordHash: String, 13 | blogs: [ 14 | { 15 | type: mongoose.Schema.Types.ObjectId, 16 | ref: 'Blog' 17 | } 18 | ], 19 | }) 20 | 21 | userSchema.plugin(uniqueValidator) 22 | 23 | userSchema.set('toJSON', { 24 | transform: (document, returnedObject) => { 25 | returnedObject.id = returnedObject._id.toString() 26 | delete returnedObject._id 27 | delete returnedObject.__v 28 | delete returnedObject.passwordHash 29 | } 30 | }) 31 | 32 | const User = mongoose.model('BlogUser', userSchema) 33 | 34 | module.exports = User -------------------------------------------------------------------------------- /part_7/bloglist-backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bloglist-backend", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index.js", 8 | "watch": "nodemon index.js", 9 | "test": "jest . --verbose", 10 | "test:debug": "node --inspect-brk node_modules/.bin/jest --runInBand" 11 | }, 12 | "author": "", 13 | "license": "ISC", 14 | "devDependencies": { 15 | "jest": "^24.8.0", 16 | "nodemon": "^1.19.1", 17 | "supertest": "^4.0.2" 18 | }, 19 | "dependencies": { 20 | "bcrypt": "^3.0.6", 21 | "cors": "^2.8.5", 22 | "dotenv": "^8.0.0", 23 | "express": "^4.17.1", 24 | "jsonwebtoken": "^8.5.1", 25 | "mongoose": "^5.6.2", 26 | "mongoose-unique-validator": "^2.0.3" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /part_7/bloglist-backend/tests/test_helper.js: -------------------------------------------------------------------------------- 1 | const Blog = require('../models/blog') 2 | const User = require('../models/user') 3 | 4 | const initialBlogs = [ 5 | { 6 | title: "React patterns", 7 | author: "Michael Chan", 8 | url: "https://reactpatterns.com/", 9 | likes: 7, 10 | }, 11 | { 12 | title: "Go To Statement Considered Harmful", 13 | author: "Edsger W. Dijkstra", 14 | url: "http://www.u.arizona.edu/~rubinson/copyright_violations/Go_To_Considered_Harmful.html", 15 | likes: 5, 16 | }, 17 | ] 18 | 19 | const blogsInDb = async () => { 20 | const blogs = await Blog.find({}) 21 | return blogs.map(b => b.toJSON()) 22 | } 23 | 24 | const usersInDb = async () => { 25 | const users = await User.find({}) 26 | return users.map(u => u.toJSON()) 27 | } 28 | 29 | const equalTo = (blog) => (b) => 30 | b.author === blog.author && b.title === blog.title && b.url === blog.url 31 | 32 | module.exports = { 33 | initialBlogs, 34 | blogsInDb, 35 | equalTo, 36 | usersInDb 37 | } -------------------------------------------------------------------------------- /part_7/bloglist-backend/utils/config.js: -------------------------------------------------------------------------------- 1 | if (process.env.NODE_ENV !== 'production') { 2 | require('dotenv').config() 3 | } 4 | 5 | let PORT = process.env.PORT 6 | let MONGODB_URI = process.env.MONGODB_URI 7 | 8 | if (process.env.NODE_ENV === 'test') { 9 | PORT = process.env.TEST_PORT 10 | MONGODB_URI = process.env.TEST_MONGODB_URI 11 | } 12 | 13 | module.exports = { 14 | MONGODB_URI, 15 | PORT 16 | } -------------------------------------------------------------------------------- /part_7/bloglist-backend/utils/helper.js: -------------------------------------------------------------------------------- 1 | const config = require('./config') 2 | const User = require('../models/user') 3 | const mongoose = require('mongoose') 4 | 5 | console.log('HELPER') 6 | 7 | mongoose.connect(config.MONGODB_URI, { useNewUrlParser: true }) 8 | .then(() => { 9 | User.findByIdAndDelete('5c4857c4003ad1a6e6626932').then(resp => { 10 | console.log(resp) 11 | mongoose.connection.close() 12 | }) 13 | }) 14 | .catch((error) => { 15 | console.log('error connection to MongoDB:', error.message) 16 | }) 17 | -------------------------------------------------------------------------------- /part_7/bloglist-backend/utils/list_helper.js: -------------------------------------------------------------------------------- 1 | const dummy = (blogs) => 1 2 | 3 | const byLikes = (a, b) => b.likes - a.likes 4 | const byValue = (a, b) => b.value - a.value 5 | 6 | const authorWithGreatest = (counts) => { 7 | const authorValues = Object.keys(counts).map(author => ({ 8 | author, 9 | value: counts[author] 10 | })) 11 | 12 | return authorValues.sort(byValue)[0] 13 | } 14 | 15 | const totalLikes = (blogs) => { 16 | if (blogs.length === 0) { 17 | return 0 18 | } 19 | 20 | return blogs.reduce((s, b) => s + b.likes, 0) 21 | } 22 | 23 | const favoriteBlog = (blogs) => { 24 | if (blogs.length === 0) { 25 | return null 26 | } 27 | 28 | const { title, author, likes } = blogs.sort(byLikes)[0] 29 | 30 | return { title, author, likes } 31 | } 32 | 33 | const mostBlogs = (blogs) => { 34 | if (blogs.length === 0) { 35 | return null 36 | } 37 | 38 | const blogCount = blogs.reduce((obj, blog) => { 39 | if (obj[blog.author] === undefined) { 40 | obj[blog.author] = 0 41 | } 42 | 43 | obj[blog.author] += 1 44 | 45 | return obj 46 | }, {}) 47 | 48 | const { author, value } = authorWithGreatest(blogCount) 49 | 50 | return { 51 | author, 52 | blogs: value 53 | } 54 | } 55 | 56 | const mostLikes = (blogs) => { 57 | if (blogs.length === 0) { 58 | return null 59 | } 60 | 61 | const likeCount = blogs.reduce((obj, blog) => { 62 | if (obj[blog.author] === undefined) { 63 | obj[blog.author] = 0 64 | } 65 | obj[blog.author] += blog.likes 66 | 67 | return obj 68 | }, {}) 69 | 70 | const { author, value } = authorWithGreatest(likeCount) 71 | 72 | return { 73 | author, 74 | likes: value 75 | } 76 | } 77 | 78 | module.exports = { 79 | dummy, 80 | totalLikes, 81 | favoriteBlog, 82 | mostBlogs, 83 | mostLikes 84 | } -------------------------------------------------------------------------------- /part_7/bloglist-backend/utils/middleware.js: -------------------------------------------------------------------------------- 1 | const getTokenFrom = request => { 2 | const authorization = request.get('authorization') 3 | if (authorization && authorization.toLowerCase().startsWith('bearer ')) { 4 | return authorization.substring(7) 5 | } 6 | return null 7 | } 8 | 9 | const tokenExtractor = (request, response, next) => { 10 | request.token = getTokenFrom(request) 11 | next() 12 | } 13 | 14 | const errorHandler = (error, request, response, next) => { 15 | if (error.name === 'CastError' && error.kind === 'ObjectId') { 16 | return response.status(400).send({ error: 'malformatted id' }) 17 | } else if (error.name === 'ValidationError') { 18 | return response.status(400).json({ error: error.message }) 19 | } else if (error.name === 'JsonWebTokenError') { 20 | return response.status(401).json({ error: 'invalid token' }) 21 | } 22 | 23 | console.error(error.message) 24 | 25 | next(error) 26 | } 27 | 28 | module.exports = { 29 | errorHandler, 30 | tokenExtractor 31 | } -------------------------------------------------------------------------------- /part_7/bloglist-backend/utils/test_helper.js: -------------------------------------------------------------------------------- 1 | const Blog = require('../models/blog') 2 | const User = require('../models/user') 3 | 4 | const initialBlogs = [ 5 | { 6 | title: "React patterns", 7 | author: "Michael Chan", 8 | url: "https://reactpatterns.com/", 9 | likes: 7, 10 | }, 11 | { 12 | title: "Go To Statement Considered Harmful", 13 | author: "Edsger W. Dijkstra", 14 | url: "http://www.u.arizona.edu/~rubinson/copyright_violations/Go_To_Considered_Harmful.html", 15 | likes: 5, 16 | }, 17 | ] 18 | 19 | const blogsInDb = async () => { 20 | const blogs = await Blog.find({}) 21 | return blogs.map(b => b.toJSON()) 22 | } 23 | 24 | const usersInDb = async () => { 25 | const users = await User.find({}) 26 | return users.map(u => u.toJSON()) 27 | } 28 | 29 | const equalTo = (blog) => (b) => 30 | b.author === blog.author && b.title === blog.title && b.url === blog.url 31 | 32 | module.exports = { 33 | initialBlogs, 34 | blogsInDb, 35 | equalTo, 36 | usersInDb 37 | } -------------------------------------------------------------------------------- /part_7/bloglist-frontend/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | commonjs: true, 4 | es6: true, 5 | "browser": true, 6 | node: true, 7 | jest: true 8 | }, 9 | extends: ["eslint:recommended", "plugin:react/recommended"], 10 | globals: { 11 | Atomics: "readonly", 12 | SharedArrayBuffer: "readonly" 13 | }, 14 | parserOptions: { 15 | ecmaVersion: 2018, 16 | sourceType: "module" 17 | }, 18 | rules: { 19 | indent: ["error", 4], 20 | "linebreak-style": ["error", "unix"], 21 | quotes: ["error", "single"], 22 | semi: ["error", "never"], 23 | eqeqeq: "error", 24 | "no-trailing-spaces": "error", 25 | "object-curly-spacing": ["error", "always"], 26 | "arrow-spacing": ["error", { before: true, after: true }], 27 | "no-console": 0, 28 | "react/prop-types": 0 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /part_7/bloglist-frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bloglist-frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "axios": "^0.19.0", 7 | "prop-types": "^15.7.2", 8 | "react": "^16.8.6", 9 | "react-dom": "^16.8.6", 10 | "react-redux": "^7.1.0", 11 | "react-router-dom": "^5.0.1", 12 | "react-scripts": "3.0.1", 13 | "redux": "^4.0.1", 14 | "redux-thunk": "^2.3.0", 15 | "semantic-ui-react": "^0.87.2" 16 | }, 17 | "scripts": { 18 | "start": "react-scripts start", 19 | "build": "react-scripts build", 20 | "test": "react-scripts test", 21 | "eject": "react-scripts eject", 22 | "lint": "eslint ." 23 | }, 24 | "eslintConfig": { 25 | "extends": "react-app" 26 | }, 27 | "proxy": "http://localhost:3001", 28 | "browserslist": [ 29 | ">0.2%", 30 | "not dead", 31 | "not ie <= 11", 32 | "not op_mini all" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /part_7/bloglist-frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/villeheikkila/fullstackopen/7d62b2885996e6e68a96fb4cf0133fb371095767/part_7/bloglist-frontend/public/favicon.ico -------------------------------------------------------------------------------- /part_7/bloglist-frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 14 | 15 | 24 | React App 25 | 26 | 27 | 28 | 29 |
    30 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /part_7/bloglist-frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /part_7/bloglist-frontend/src/components/Blog.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | import { createNotification } from '../reducers/notificationReducer' 4 | import { createBlog, deleteBlog, updateBlog } from '../reducers/blogReducer' 5 | import { Button } from 'semantic-ui-react' 6 | 7 | const Blog = (props) => { 8 | if (props.blog === undefined) { 9 | return null 10 | } 11 | 12 | const { blog } = props 13 | 14 | const likeBlog = async (blog) => { 15 | const likedBlog = { ...blog, likes: blog.likes + 1 } 16 | props.updateBlog(likedBlog) 17 | props.createNotification({ message: `blog ${blog.title} by ${blog.author} liked!`, type: 'success' }, 2) 18 | 19 | } 20 | 21 | const removeBlog = async (blog) => { 22 | const ok = window.confirm(`remove blog ${blog.title} by ${blog.author}`) 23 | if (ok) { 24 | props.deleteBlog(blog) 25 | props.createNotification({ message: `blog ${blog.title} by ${blog.author} removed!`, type: 'success' }, 2) 26 | } 27 | } 28 | 29 | return ( 30 |
    31 |

    {blog.title}

    32 | {blog.url} 33 |
    34 | {blog.likes} likes 35 |
    added by {blog.user.name}
    36 | {blog.user.username === props.user.username ? () : (null)} 37 |
    38 | ) 39 | } 40 | 41 | const mapStateToProps = (state) => { 42 | return { 43 | notification: state.notification, 44 | blogs: state.blogs, 45 | user: state.user 46 | } 47 | } 48 | const mapDispatchToProps = { createNotification, createBlog, deleteBlog, updateBlog } 49 | 50 | const ConnectedBlog = connect(mapStateToProps, mapDispatchToProps)(Blog) 51 | export default ConnectedBlog -------------------------------------------------------------------------------- /part_7/bloglist-frontend/src/components/BlogList.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import NewBlog from './NewBlog' 3 | import Togglable from './Togglable' 4 | import { Table } from 'semantic-ui-react' 5 | import { connect } from 'react-redux' 6 | import { createNotification } from '../reducers/notificationReducer' 7 | import { createBlog, deleteBlog, updateBlog } from '../reducers/blogReducer' 8 | import { Link } from 'react-router-dom' 9 | 10 | const BlogList = (props) => { 11 | const handleCreateBlog = async (blog) => { 12 | newBlogRef.current.toggleVisibility() 13 | props.createBlog(blog) 14 | props.createNotification({ message: `a new blog ${blog.title} by ${blog.author} added`, type: 'success' }, 2) 15 | } 16 | 17 | const newBlogRef = React.createRef() 18 | 19 | const byLikes = (b1, b2) => b2.likes - b1.likes 20 | 21 | return ( 22 |
    23 |

    All blogs

    24 | 25 | 26 | 27 | 28 | 29 | {props.blogs.sort(byLikes).map(blog => 30 | 31 | 32 | {blog.title} 33 | 34 | 35 | )} 36 | 37 |
    38 |
    39 | ) 40 | } 41 | const mapStateToProps = (state) => { 42 | return { 43 | notification: state.notification, 44 | blogs: state.blogs, 45 | user: state.user 46 | } 47 | } 48 | const mapDispatchToProps = { createNotification, createBlog, deleteBlog, updateBlog } 49 | 50 | const ConnectedBlogList = connect(mapStateToProps, mapDispatchToProps)(BlogList) 51 | export default ConnectedBlogList -------------------------------------------------------------------------------- /part_7/bloglist-frontend/src/components/Login.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Form, Button } from 'semantic-ui-react' 3 | import { useField } from '../hooks' 4 | import { connect } from 'react-redux' 5 | import { login } from '../reducers/userReducer' 6 | import { createNotification } from '../reducers/notificationReducer' 7 | 8 | const Login = (props) => { 9 | const [username] = useField('text') 10 | const [password] = useField('password') 11 | 12 | const handleLogin = async (event) => { 13 | event.preventDefault() 14 | const response = await props.login({ username: username.value, password: password.value }) 15 | if (response !== undefined) { 16 | props.createNotification({ message: 'wrong username or password', type: 'error' }, 2) 17 | } 18 | } 19 | 20 | return ( 21 |
    22 |

    log in to application

    23 | 24 |
    25 |
    26 | käyttäjätunnus 27 | 28 |
    29 |
    30 | salasana 31 | 32 |
    33 | 34 |
    35 |
    36 | ) 37 | } 38 | 39 | const mapStateToProps = (state) => { 40 | return { 41 | user: state.user, 42 | notification: state.notification, 43 | } 44 | } 45 | 46 | const mapDispatchToProps = { createNotification, login } 47 | 48 | const ConnectedLogin = connect(mapStateToProps, mapDispatchToProps)(Login) 49 | export default ConnectedLogin -------------------------------------------------------------------------------- /part_7/bloglist-frontend/src/components/NavBar.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'react-router-dom' 3 | import { Menu, Button } from 'semantic-ui-react' 4 | import blogService from '../services/blogs' 5 | import { logout } from '../reducers/userReducer' 6 | import { connect } from 'react-redux' 7 | 8 | const NavBar = (props) => { 9 | const handleLogout = () => { 10 | props.logout() 11 | blogService.destroyToken() 12 | window.localStorage.removeItem('loggedBlogAppUser') 13 | } 14 | 15 | const style = { 16 | color: 'white', 17 | fontWeight: '600' 18 | } 19 | 20 | const buttonStyle = { 21 | backgroundColor: '#EA7171', 22 | color: '#fff', 23 | margin: '5px 10px', 24 | } 25 | return ( 26 | 27 | 28 | Blogs 29 | 30 | 31 | Users 32 | 33 | 34 | {props.user.name} logged in 35 | 36 | 37 | 38 | ) 39 | } 40 | 41 | const mapStateToProps = (state) => { 42 | return { 43 | user: state.user, 44 | notification: state.notification, 45 | } 46 | } 47 | 48 | const mapDispatchToProps = { logout } 49 | 50 | const ConnectedNavBar = connect(mapStateToProps, mapDispatchToProps)(NavBar) 51 | export default ConnectedNavBar -------------------------------------------------------------------------------- /part_7/bloglist-frontend/src/components/NewBlog.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useField } from '../hooks' 3 | import { Form, Button } from 'semantic-ui-react' 4 | 5 | const NewBlog = (props) => { 6 | const [title, titleReset] = useField('text') 7 | const [author, authorReset] = useField('text') 8 | const [url, urlReset] = useField('text') 9 | 10 | const handleSubmit = (event) => { 11 | event.preventDefault() 12 | props.createBlog({ 13 | title: title.value, 14 | author: author.value, 15 | url: url.value 16 | }) 17 | titleReset() 18 | authorReset() 19 | urlReset() 20 | } 21 | 22 | const style = { 23 | backgroundColor: '#EA7171', 24 | color: '#fff', 25 | margin: '5px 0px' 26 | } 27 | 28 | return ( 29 |
    30 |

    create new

    31 | 32 |
    33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 |
    47 |
    48 | ) 49 | } 50 | 51 | export default NewBlog -------------------------------------------------------------------------------- /part_7/bloglist-frontend/src/components/Notification.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | import { Container, Message } from 'semantic-ui-react' 4 | 5 | const Notification = (props) => { 6 | if (props.notification === '') { 7 | return null 8 | } 9 | 10 | return ( 11 | 12 | 13 | {props.notification.message} 14 | 15 | 16 | ) 17 | } 18 | 19 | const mapStateToProps = (state) => { 20 | return { 21 | notification: state.notification, 22 | } 23 | } 24 | 25 | const ConnectedNotification = connect(mapStateToProps)(Notification) 26 | export default ConnectedNotification -------------------------------------------------------------------------------- /part_7/bloglist-frontend/src/components/Togglable.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useImperativeHandle } from 'react' 2 | import { Button } from 'semantic-ui-react' 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 | 26 |
    27 |
    28 | {props.children} 29 | 30 |
    31 |
    32 | ) 33 | }) 34 | 35 | export default Togglable -------------------------------------------------------------------------------- /part_7/bloglist-frontend/src/components/User.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Table } from 'semantic-ui-react' 3 | 4 | const User = (props) => { 5 | if (props.user === undefined) { 6 | return null 7 | } 8 | 9 | return ( 10 |
    11 |

    props.name

    12 | 13 | 14 | 15 | Added blogs 16 | 17 | 18 | 19 | {props.user.blogs.map(blog => 20 | 21 | 22 | {blog.title} 23 | 24 | 25 | )} 26 | 27 |
    28 |
    29 | ) 30 | } 31 | 32 | export default User -------------------------------------------------------------------------------- /part_7/bloglist-frontend/src/components/Users.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | import { Table } from 'semantic-ui-react' 4 | import { Link } from 'react-router-dom' 5 | 6 | const Users = (props) => { 7 | return ( 8 |
    9 |

    Users

    10 | 11 | 12 | 13 | Name 14 | Number of blogs 15 | 16 | 17 | 18 | {props.users.map(user => 19 | 20 | 21 | {user.name} 22 | 23 | 24 | {user.blogs.length} 25 | 26 | 27 | )} 28 | 29 |
    30 |
    31 | ) 32 | } 33 | 34 | const mapStateToProps = (state) => { 35 | return { 36 | users: state.users 37 | } 38 | } 39 | 40 | const ConnectedUsers = connect(mapStateToProps)(Users) 41 | export default ConnectedUsers -------------------------------------------------------------------------------- /part_7/bloglist-frontend/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 | onChange, 18 | }, reset] 19 | } -------------------------------------------------------------------------------- /part_7/bloglist-frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import App from './App' 4 | import { Provider } from 'react-redux' 5 | import store from './store' 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root') 12 | ) 13 | -------------------------------------------------------------------------------- /part_7/bloglist-frontend/src/reducers/blogReducer.js: -------------------------------------------------------------------------------- 1 | import blogService from '../services/blogs' 2 | 3 | const getId = () => (100000 * Math.random()).toFixed(0) 4 | 5 | const asObject = (data) => { 6 | return { 7 | title: data.title, 8 | author: data.author, 9 | url: data.url, 10 | id: getId() 11 | } 12 | } 13 | 14 | export const createBlog = (content) => { 15 | return async dispatch => { 16 | const createdBlog = await blogService.create(asObject(content)) 17 | dispatch({ 18 | type: 'NEW_BLOG', 19 | data: createdBlog 20 | }) 21 | } 22 | } 23 | 24 | export const updateBlog = (blog) => { 25 | return async dispatch => { 26 | const updatedBlog = await blogService.update(blog) 27 | dispatch({ 28 | type: 'UPDATE_BLOG', 29 | data: updatedBlog 30 | }) 31 | } 32 | } 33 | 34 | export const deleteBlog = (blog) => { 35 | console.log('blasdsadasdsaog: ', blog) 36 | return async dispatch => { 37 | await blogService.remove(blog) 38 | dispatch({ 39 | type: 'DELETE_BLOG', 40 | data: blog 41 | }) 42 | } 43 | } 44 | 45 | export const initializeBlogs = () => { 46 | return async dispatch => { 47 | const blogs = await blogService.getAll() 48 | console.log('blogs: ', blogs) 49 | dispatch({ 50 | type: 'INIT_BLOGS', 51 | data: blogs, 52 | }) 53 | } 54 | } 55 | 56 | const blogReducer = (state = [], action) => { 57 | switch (action.type) { 58 | case 'NEW_BLOG': 59 | return [...state, action.data] 60 | case 'UPDATE_BLOG': { 61 | const updatedBlogs = state.map(b => b.id === action.data.id ? action.data : b) 62 | return updatedBlogs 63 | } 64 | case 'DELETE_BLOG': { 65 | const newBlogs = state.filter(b => b.id !== action.data.id) 66 | return newBlogs 67 | } 68 | case 'INIT_BLOGS': 69 | return action.data 70 | default: 71 | return state 72 | } 73 | } 74 | 75 | export default blogReducer -------------------------------------------------------------------------------- /part_7/bloglist-frontend/src/reducers/notificationReducer.js: -------------------------------------------------------------------------------- 1 | 2 | export const createNotification = (content, time) => { 3 | return async dispatch => { 4 | dispatch({ 5 | type: 'SET_NOTIFICATION', 6 | data: { message: content.message, type: content.type } 7 | }) 8 | setTimeout(() => { 9 | dispatch({ 10 | type: 'DELETE_NOTIFICATION', 11 | }) 12 | }, time * 1000) 13 | } 14 | } 15 | 16 | export const deleteNotification = () => { 17 | return { 18 | type: 'DELETE_NOTIFICATION', 19 | } 20 | } 21 | 22 | const notificationReducer = (state = '', action) => { 23 | switch (action.type) { 24 | case 'SET_NOTIFICATION': 25 | return action.data 26 | case 'DELETE_NOTIFICATION': 27 | return '' 28 | default: 29 | return state 30 | } 31 | } 32 | 33 | export default notificationReducer -------------------------------------------------------------------------------- /part_7/bloglist-frontend/src/reducers/userReducer.js: -------------------------------------------------------------------------------- 1 | import loginService from '../services/login' 2 | import blogService from '../services/blogs' 3 | 4 | export const setUser = (content) => { 5 | return async dispatch => { 6 | dispatch({ 7 | type: 'SET_USER', 8 | data: { name: content.name, username: content.username, token: content.token } 9 | }) 10 | } 11 | } 12 | 13 | export const login = (content) => { 14 | return async dispatch => { 15 | try { 16 | const user = await loginService.login({ 17 | username: content.username, 18 | password: content.password 19 | }) 20 | window.localStorage.setItem('loggedBlogAppUser', JSON.stringify(user)) 21 | blogService.setToken(user.token) 22 | dispatch({ 23 | type: 'SET_USER', 24 | data: user 25 | }) 26 | } catch (exception) { 27 | return exception 28 | } 29 | } 30 | } 31 | 32 | export const logout = () => { 33 | return { 34 | type: 'LOGOUT', 35 | } 36 | } 37 | 38 | const userReducer = (state = null, action) => { 39 | switch (action.type) { 40 | case 'SET_USER': 41 | return action.data 42 | case 'LOGOUT': 43 | return null 44 | default: 45 | return state 46 | } 47 | } 48 | 49 | export default userReducer -------------------------------------------------------------------------------- /part_7/bloglist-frontend/src/reducers/usersReducer.js: -------------------------------------------------------------------------------- 1 | import usersService from '../services/users' 2 | 3 | export const initializeUsers = () => { 4 | return async dispatch => { 5 | const users = await usersService.getAll() 6 | console.log('uadssadasdsers: ', users) 7 | dispatch({ 8 | type: 'INIT_USERS', 9 | data: users, 10 | }) 11 | } 12 | } 13 | 14 | const blogReducer = (state = [], action) => { 15 | switch (action.type) { 16 | case 'INIT_USERS': 17 | return action.data 18 | default: 19 | return state 20 | } 21 | } 22 | 23 | export default blogReducer -------------------------------------------------------------------------------- /part_7/bloglist-frontend/src/services/blogs.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | const baseUrl = '/api/blogs' 3 | 4 | let token = null 5 | 6 | const getConfig = () => ({ 7 | headers: { Authorization: token } 8 | }) 9 | 10 | const setToken = newToken => { 11 | token = `bearer ${newToken}` 12 | } 13 | 14 | const destroyToken = () => { 15 | token = null 16 | } 17 | 18 | const getAll = () => { 19 | const request = axios.get(baseUrl) 20 | return request.then(response => response.data) 21 | } 22 | 23 | const create = async newObject => { 24 | const response = await axios.post(baseUrl, newObject, getConfig()) 25 | return response.data 26 | } 27 | 28 | const update = async newObject => { 29 | const response = await axios.put(`${baseUrl}/${newObject.id}`, newObject, getConfig()) 30 | return response.data 31 | } 32 | 33 | const remove = async object => { 34 | const response = await axios.delete(`${baseUrl}/${object.id}`, getConfig()) 35 | return response.data 36 | } 37 | 38 | export default { getAll, create, update, remove, setToken, destroyToken } -------------------------------------------------------------------------------- /part_7/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 } -------------------------------------------------------------------------------- /part_7/bloglist-frontend/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 } -------------------------------------------------------------------------------- /part_7/bloglist-frontend/src/store.js: -------------------------------------------------------------------------------- 1 | 2 | import { createStore, combineReducers, applyMiddleware } from 'redux' 3 | import thunk from 'redux-thunk' 4 | 5 | import notificationReducer from './reducers/notificationReducer' 6 | import blogReducer from './reducers/blogReducer' 7 | import userReducer from './reducers/userReducer' 8 | import usersReducer from './reducers/usersReducer' 9 | 10 | const reducer = combineReducers({ 11 | notification: notificationReducer, 12 | blogs: blogReducer, 13 | user: userReducer, 14 | users: usersReducer 15 | }) 16 | 17 | const store = createStore(reducer, applyMiddleware(thunk)) 18 | store.subscribe(() => { 19 | const storeNow = store.getState() 20 | console.log('global', storeNow) 21 | }) 22 | export default store -------------------------------------------------------------------------------- /part_7/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 | -------------------------------------------------------------------------------- /part_7/routed-anecdotes/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "routed-anecdotes", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "react": "^16.8.6", 7 | "react-dom": "^16.8.6", 8 | "react-router-dom": "^5.0.1", 9 | "react-scripts": "3.0.1" 10 | }, 11 | "scripts": { 12 | "start": "react-scripts start", 13 | "build": "react-scripts build", 14 | "test": "react-scripts test", 15 | "eject": "react-scripts eject" 16 | }, 17 | "eslintConfig": { 18 | "extends": "react-app" 19 | }, 20 | "browserslist": [ 21 | ">0.2%", 22 | "not dead", 23 | "not ie <= 11", 24 | "not op_mini all" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /part_7/routed-anecdotes/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/villeheikkila/fullstackopen/7d62b2885996e6e68a96fb4cf0133fb371095767/part_7/routed-anecdotes/public/favicon.ico -------------------------------------------------------------------------------- /part_7/routed-anecdotes/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 15 | 16 | 25 | React App 26 | 27 | 28 | 29 |
    30 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /part_7/routed-anecdotes/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /part_7/routed-anecdotes/src/index.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import ReactDOM from 'react-dom' 4 | import App from './App' 5 | 6 | ReactDOM.render(, document.getElementById('root')) -------------------------------------------------------------------------------- /part_8/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) -------------------------------------------------------------------------------- /part_8/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) -------------------------------------------------------------------------------- /part_8/library-backend/models/user.js: -------------------------------------------------------------------------------- 1 | 2 | const mongoose = require('mongoose') 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 | minlength: 3 15 | } 16 | }) 17 | 18 | module.exports = mongoose.model('User', schema) -------------------------------------------------------------------------------- /part_8/library-backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "books", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "watch": "nodemon library-backend.js" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "apollo-server": "^2.7.0-alpha.3", 14 | "cors": "^2.8.5", 15 | "dotenv": "^8.0.0", 16 | "graphql": "^14.4.1", 17 | "jsonwebtoken": "^8.5.1", 18 | "mongoose": "^5.6.2", 19 | "mongoose-unique-validator": "^2.0.3", 20 | "nodemon": "^1.19.1" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /part_8/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 | -------------------------------------------------------------------------------- /part_8/library-frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fronend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "apollo-boost": "^0.4.3", 7 | "apollo-link": "^1.2.12", 8 | "apollo-link-context": "^1.0.18", 9 | "apollo-link-ws": "^1.0.18", 10 | "graphql": "^14.3.1", 11 | "react": "^16.8.6", 12 | "react-apollo": "^3.0.0-beta.2", 13 | "react-dom": "^16.8.6", 14 | "react-scripts": "3.0.1", 15 | "subscriptions-transport-ws": "^0.9.16" 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 | "proxy": "http://localhost:4000", 27 | "browserslist": { 28 | "production": [ 29 | ">0.2%", 30 | "not dead", 31 | "not op_mini all" 32 | ], 33 | "development": [ 34 | "last 1 chrome version", 35 | "last 1 firefox version", 36 | "last 1 safari version" 37 | ] 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /part_8/library-frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/villeheikkila/fullstackopen/7d62b2885996e6e68a96fb4cf0133fb371095767/part_8/library-frontend/public/favicon.ico -------------------------------------------------------------------------------- /part_8/library-frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 22 | React App 23 | 24 | 25 | 26 |
    27 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /part_8/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 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /part_8/library-frontend/src/components/Authors.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | 3 | const Authors = (props) => { 4 | const [name, setName] = useState('') 5 | const [born, setBorn] = useState('') 6 | 7 | if (!props.show) { 8 | return null 9 | } 10 | 11 | if (props.result.loading) { 12 | return
    loading...
    13 | } 14 | 15 | const authors = props.result.data.allAuthors 16 | 17 | const handleChange = (event) => { 18 | setName(event.target.value) 19 | } 20 | 21 | const submit = async (e) => { 22 | e.preventDefault() 23 | 24 | await props.editAuthor({ 25 | variables: { name, "born": parseInt(born) } 26 | }) 27 | 28 | setName('') 29 | setBorn('') 30 | } 31 | 32 | return ( 33 |
    34 |

    authors

    35 | 36 | 37 | 38 | 39 | 42 | 45 | 46 | {authors.map(a => 47 | 48 | 49 | 50 | 51 | 52 | )} 53 | 54 |
    40 | born 41 | 43 | books 44 |
    {a.name}{a.born}{a.bookCount}
    55 | {props.token !== null ? ( 56 |

    Set birthyear

    57 |
    58 |
    59 | select author: 60 | 65 |
    66 | born 67 | setBorn(target.value)} 70 | /> 71 |
    72 | 73 |
    74 |
    ) : (null) 75 | } 76 |
    77 | ) 78 | } 79 | 80 | export default Authors -------------------------------------------------------------------------------- /part_8/library-frontend/src/components/Books.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | 3 | const Books = ({ result, show }) => { 4 | const [filter, setFilter] = useState("all genres") 5 | 6 | if (!show) { 7 | return null 8 | } 9 | 10 | if (result.loading) { 11 | return
    loading...
    12 | } 13 | 14 | const books = result.data.allBooks 15 | const genres = ["refactoring", "agile", "patterns", "design", "crime", "classic", "all genres"] 16 | 17 | return ( 18 |
    19 |

    books

    20 | {filter !== "all genres" ?

    in genre {filter}

    : (null)} 21 | < table > 22 | 23 | 24 | 25 | 26 | author 27 | 28 | 29 | published 30 | 31 | 32 | {books.filter(book => filter === "all genres" ? book : book.genres.includes(filter)).map(a => 33 | 34 | {a.title} 35 | {a.author.name} 36 | {a.published} 37 | 38 | )} 39 | 40 | 41 | {genres.map(a => 42 | 43 | )} 44 |
    45 | ) 46 | } 47 | 48 | export default Books -------------------------------------------------------------------------------- /part_8/library-frontend/src/components/Login.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | 3 | const Login = (props) => { 4 | const [username, setUsername] = useState('') 5 | const [password, setPassword] = useState('') 6 | 7 | if (!props.show) { 8 | return null 9 | } 10 | 11 | const submit = async (event) => { 12 | event.preventDefault() 13 | 14 | const result = await props.login({ 15 | variables: { username, password } 16 | }) 17 | 18 | if (result) { 19 | const token = result.data.login.value 20 | props.setToken(token) 21 | localStorage.setItem('token', token) 22 | } 23 | } 24 | 25 | return ( 26 |
    27 |
    28 |
    29 | username setUsername(target.value)} 32 | /> 33 |
    34 |
    35 | password setPassword(target.value)} 39 | /> 40 |
    41 | 42 |
    43 |
    44 | ) 45 | } 46 | 47 | export default Login -------------------------------------------------------------------------------- /part_8/library-frontend/src/components/NewBook.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | 3 | const NewBook = (props) => { 4 | const [title, setTitle] = useState('') 5 | const [author, setAuhtor] = useState('') 6 | const [published, setPublished] = useState('') 7 | const [genre, setGenre] = useState('') 8 | const [genres, setGenres] = useState([]) 9 | 10 | 11 | if (!props.show) { 12 | return null 13 | } 14 | 15 | const submit = async (e) => { 16 | e.preventDefault() 17 | 18 | await props.addBook({ 19 | variables: { title, author, "published": parseInt(published), genres } 20 | }) 21 | 22 | setTitle('') 23 | setPublished('') 24 | setAuhtor('') 25 | setGenres([]) 26 | setGenre('') 27 | } 28 | 29 | const addGenre = () => { 30 | setGenres(genres.concat(genre)) 31 | setGenre('') 32 | } 33 | 34 | return ( 35 |
    36 |
    37 |
    38 | title 39 | setTitle(target.value)} 42 | /> 43 |
    44 |
    45 | author 46 | setAuhtor(target.value)} 49 | /> 50 |
    51 |
    52 | published 53 | setPublished(target.value)} 57 | /> 58 |
    59 |
    60 | setGenre(target.value)} 63 | /> 64 | 65 |
    66 |
    67 | genres: {genres.join(' ')} 68 |
    69 | 70 |
    71 |
    72 | ) 73 | } 74 | 75 | export default NewBook -------------------------------------------------------------------------------- /part_8/library-frontend/src/components/RecommendedBooks.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const RecommendedBooks = ({ result, show, user }) => { 4 | if (!show || !user) { 5 | return null 6 | } 7 | 8 | if (result.loading) { 9 | return
    loading...
    10 | } 11 | 12 | const books = result.data.allBooks 13 | 14 | return ( 15 |
    16 |

    recommendations

    17 |

    books in your favorite genre {user.favoriteGenre}

    18 | 19 | 20 | 21 | 22 | 23 | 26 | 29 | 30 | {books.filter(book => book.genres.includes(user.favoriteGenre)).map(a => 31 | 32 | 33 | 34 | 35 | 36 | )} 37 | 38 |
    24 | author 25 | 27 | published 28 |
    {a.title}{a.author.name}{a.published}
    39 |
    40 | ) 41 | } 42 | 43 | export default RecommendedBooks -------------------------------------------------------------------------------- /part_8/library-frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import App from './App' 4 | import { ApolloProvider } from 'react-apollo' 5 | import { ApolloClient } from 'apollo-client' 6 | import { createHttpLink } from 'apollo-link-http' 7 | import { InMemoryCache } from 'apollo-cache-inmemory' 8 | import { setContext } from 'apollo-link-context' 9 | import { split } from 'apollo-link' 10 | import { WebSocketLink } from 'apollo-link-ws' 11 | import { getMainDefinition } from 'apollo-utilities' 12 | 13 | const wsLink = new WebSocketLink({ 14 | uri: `ws://localhost:4000/graphql`, 15 | options: { reconnect: true } 16 | }) 17 | 18 | 19 | const httpLink = createHttpLink({ 20 | uri: 'http://localhost:4000/', 21 | }) 22 | 23 | const authLink = setContext((_, { headers }) => { 24 | const token = localStorage.getItem('token') 25 | return { 26 | headers: { 27 | ...headers, 28 | authorization: token ? `bearer ${token}` : null, 29 | } 30 | } 31 | }) 32 | 33 | const link = split( 34 | ({ query }) => { 35 | const { kind, operation } = getMainDefinition(query) 36 | return kind === 'OperationDefinition' && operation === 'subscription' 37 | }, 38 | wsLink, 39 | authLink.concat(httpLink), 40 | ) 41 | 42 | 43 | const client = new ApolloClient({ 44 | link, 45 | cache: new InMemoryCache() 46 | }) 47 | 48 | ReactDOM.render( 49 | 50 | 51 | 52 | , document.getElementById('root')) -------------------------------------------------------------------------------- /part_9/exercises_9.1-9.7/.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/explicit-function-return-type": "off", 15 | "@typescript-eslint/explicit-module-boundary-types": "off", 16 | "@typescript-eslint/restrict-template-expressions": "off", 17 | "@typescript-eslint/restrict-plus-operands": "off", 18 | "@typescript-eslint/no-unused-vars": [ 19 | "error", 20 | { "argsIgnorePattern": "^_" } 21 | ], 22 | "no-case-declarations": "off" 23 | }, 24 | "parser": "@typescript-eslint/parser", 25 | "parserOptions": { 26 | "project": "./tsconfig.json" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /part_9/exercises_9.1-9.7/bmiCalculator.ts: -------------------------------------------------------------------------------- 1 | const bmiValues = [ 2 | "Underweight", 3 | "Normal", 4 | "Overweight", 5 | "Class 1 Obesity", 6 | "Class 2 Obesity", 7 | "Class 3 Obesity", 8 | ]; 9 | 10 | const calculateBmi = (height: number, weight: number) => { 11 | const bmi = weight / Math.pow(height / 100, 2); 12 | let prefix = ""; 13 | 14 | if (bmi < 18.5) { 15 | prefix = bmiValues[0]; 16 | } else if (bmi >= 18.5 && bmi <= 24.9) { 17 | prefix = bmiValues[1]; 18 | } else if (bmi >= 25 && bmi <= 29.9) { 19 | prefix = bmiValues[2]; 20 | } else if (bmi >= 30.0 && bmi <= 34.9) { 21 | prefix = bmiValues[3]; 22 | } else if (bmi >= 35.0 && bmi <= 39.9) { 23 | prefix = bmiValues[4]; 24 | } else if (bmi >= 40) { 25 | prefix = bmiValues[5]; 26 | } 27 | 28 | return `${prefix} (${bmi.toFixed(2)})`; 29 | }; 30 | 31 | export const parseAndCalculateBmi = (arg1: any, arg2: any) => { 32 | if (!arg1 || !arg2) { 33 | throw "You need to provide both height and weight"; 34 | } 35 | 36 | const height = parseFloat(arg1); 37 | const weight = parseFloat(arg2); 38 | 39 | if (Number.isNaN(height) || Number.isNaN(weight)) { 40 | throw "Both height and weight need to be numbers"; 41 | } 42 | 43 | return calculateBmi(height, weight); 44 | }; 45 | 46 | if (process.argv.length > 2) { 47 | console.log(parseAndCalculateBmi(process.argv[2], process.argv[3])); 48 | } 49 | -------------------------------------------------------------------------------- /part_9/exercises_9.1-9.7/exerciseCalculator.ts: -------------------------------------------------------------------------------- 1 | interface Review { 2 | periodLength: number; 3 | trainingDays: number; 4 | success: boolean; 5 | rating: number; 6 | ratingDescription: string; 7 | target: number; 8 | average: number; 9 | } 10 | 11 | const ratings = [ 12 | "try harder next week... bro...", 13 | "not too bad but could be better", 14 | "looks thick. solid. tight.", 15 | ]; 16 | 17 | const calculateExercises = (exercises: number[], target: number): Review => { 18 | const periodLength = exercises.length; 19 | const trainingDays = exercises.reduce( 20 | (total, day) => total + (day > 0 ? 1 : 0), 21 | 0 22 | ); 23 | 24 | const totalHours = exercises.reduce((total, hours) => total + hours, 0); 25 | 26 | const average = totalHours / periodLength; 27 | const rating = average >= target + 0.5 ? 3 : average >= target ? 2 : 1; 28 | 29 | return { 30 | periodLength, 31 | trainingDays, 32 | success: average >= target, 33 | rating, 34 | ratingDescription: ratings[rating - 1], 35 | target, 36 | average, 37 | }; 38 | }; 39 | 40 | export const parseInputCalculateExercises = ( 41 | targetRaw: any, 42 | exercisesRaw: any[] 43 | ) => { 44 | if (!targetRaw || exercisesRaw.length === 0) { 45 | throw "parameters missing"; 46 | } 47 | 48 | const exercises = exercisesRaw.map((e) => parseFloat(e)); 49 | const target = parseFloat(targetRaw); 50 | 51 | if (Number.isNaN(target) || exercises.some((e) => isNaN(e))) { 52 | throw "malformatted parameters"; 53 | } 54 | 55 | return calculateExercises(exercises, target); 56 | }; 57 | 58 | if (process.argv.length > 2) { 59 | const [, , target, ...exercises] = process.argv; 60 | 61 | console.log(parseInputCalculateExercises(target, exercises)); 62 | } 63 | -------------------------------------------------------------------------------- /part_9/exercises_9.1-9.7/index.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | const app = express(); 3 | app.use(express.json()); 4 | 5 | import { Request, Response } from "express"; 6 | import { parseAndCalculateBmi } from "./bmiCalculator"; 7 | import { parseInputCalculateExercises } from "./exerciseCalculator"; 8 | 9 | app.get("/hello", (_req: Request, res: Response) => { 10 | res.send("Hello Full Stack!"); 11 | }); 12 | 13 | app.get("/bmi", ({ query }: Request, res: Response) => { 14 | const weight = query.weight; 15 | const height = query.height; 16 | try { 17 | res.send({ weight, height, bmi: parseAndCalculateBmi(height, weight) }); 18 | } catch { 19 | res.send({ error: "malformatted parameters" }); 20 | } 21 | }); 22 | 23 | app.post("/exercises", ({ body }: Request, res: Response) => { 24 | try { 25 | // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access 26 | res.send(parseInputCalculateExercises(body?.target, body?.daily_exercises)); 27 | } catch (error) { 28 | res.send(error); 29 | } 30 | }); 31 | 32 | app.listen(3000); 33 | -------------------------------------------------------------------------------- /part_9/exercises_9.1-9.7/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "part_9", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "ts-node-dev index.ts", 8 | "ts-node": "ts-node", 9 | "calculateBmi": "ts-node bmiCalculator.ts", 10 | "calculateExercises": "ts-node exerciseCalculator.ts", 11 | "test": "echo \"Error: no test specified\" && exit 1", 12 | "lint": "eslint --ext .ts ." 13 | }, 14 | "author": "", 15 | "license": "ISC", 16 | "devDependencies": { 17 | "@types/express": "^4.17.11", 18 | "@types/node": "^14.14.35", 19 | "@typescript-eslint/eslint-plugin": "^4.18.0", 20 | "@typescript-eslint/parser": "^4.18.0", 21 | "eslint": "^7.22.0", 22 | "ts-node": "^9.1.1", 23 | "ts-node-dev": "^1.1.6", 24 | "typescript": "^4.2.3" 25 | }, 26 | "dependencies": { 27 | "express": "^4.17.1" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /part_9/exercises_9.1-9.7/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noImplicitAny": true, 4 | "noImplicitReturns": true, 5 | "strictNullChecks": true, 6 | "strictPropertyInitialization": true, 7 | "strictBindCallApply": true, 8 | "noUnusedLocals": true, 9 | "noUnusedParameters": true, 10 | "noImplicitThis": true, 11 | "alwaysStrict": true, 12 | "esModuleInterop": true, 13 | "declaration": true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /part_9/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 | "node": true 12 | }, 13 | "rules": { 14 | "@typescript-eslint/semi": ["error"], 15 | "@typescript-eslint/explicit-function-return-type": "off", 16 | "@typescript-eslint/explicit-module-boundary-types": "off", 17 | "@typescript-eslint/restrict-template-expressions": "off", 18 | "@typescript-eslint/restrict-plus-operands": "off", 19 | "@typescript-eslint/no-unsafe-member-access": "off", 20 | "@typescript-eslint/no-unused-vars": [ 21 | "error", 22 | { "argsIgnorePattern": "^_" } 23 | ], 24 | "no-case-declarations": "off" 25 | }, 26 | "parser": "@typescript-eslint/parser", 27 | "parserOptions": { 28 | "project": "./tsconfig.json" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /part_9/patientor-backend/.gitignore: -------------------------------------------------------------------------------- 1 | build -------------------------------------------------------------------------------- /part_9/patientor-backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "patientor-backend", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "tsc": "tsc", 7 | "dev": "ts-node-dev index.ts", 8 | "start": "node build/index.js", 9 | "lint": "eslint --ext .ts ." 10 | }, 11 | "author": "", 12 | "license": "ISC", 13 | "description": "", 14 | "devDependencies": { 15 | "@types/cors": "^2.8.10", 16 | "@types/express": "^4.17.11", 17 | "@types/uuid": "^8.3.0", 18 | "@typescript-eslint/eslint-plugin": "^4.18.0", 19 | "@typescript-eslint/parser": "^4.18.0", 20 | "eslint": "^7.22.0", 21 | "ts-node-dev": "^1.1.6", 22 | "typescript": "^4.2.3" 23 | }, 24 | "dependencies": { 25 | "cors": "^2.8.5", 26 | "express": "^4.17.1", 27 | "uuid": "^8.3.2" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /part_9/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 | "resolveJsonModule": true, 10 | "noImplicitReturns": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "esModuleInterop": true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /part_9/patientor-backend/types.ts: -------------------------------------------------------------------------------- 1 | export interface Diagnose { 2 | code: string; 3 | name: string; 4 | latin?: string; 5 | } 6 | 7 | export enum Gender { 8 | female = "female", 9 | male = "male", 10 | } 11 | 12 | export type BirthDate = `${number}-${number}-${number}`; 13 | 14 | export interface BaseEntry { 15 | id: string; 16 | description: string; 17 | date: string; 18 | specialist: string; 19 | diagnosisCodes?: string[]; 20 | } 21 | 22 | export enum HealthCheckRating { 23 | "Healthy" = 0, 24 | "LowRisk" = 1, 25 | "HighRisk" = 2, 26 | "CriticalRisk" = 3, 27 | } 28 | 29 | export interface SickLeave { 30 | startDate: string; 31 | endDate: string; 32 | } 33 | 34 | export interface OccupationalHealthcareEntry extends BaseEntry { 35 | type: "OccupationalHealthcare"; 36 | employerName: string; 37 | sickLeave: SickLeave; 38 | } 39 | 40 | export interface Discharge { 41 | date: string; 42 | criteria: string; 43 | } 44 | 45 | export interface HospitalEntry extends BaseEntry { 46 | type: "Hospital"; 47 | id: string; 48 | discharge: Discharge; 49 | } 50 | 51 | export type Entry = HospitalEntry | OccupationalHealthcareEntry; 52 | 53 | export interface Patient { 54 | id: string; 55 | name: string; 56 | dateOfBirth: BirthDate; 57 | ssn: string; 58 | gender: Gender; 59 | occupation: string; 60 | entries: Entry[]; 61 | } 62 | 63 | export type PublicPatient = Omit; 64 | -------------------------------------------------------------------------------- /part_9/react-exercises/.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 | "@typescript-eslint/explicit-module-boundary-types": 0 22 | } 23 | } -------------------------------------------------------------------------------- /part_9/react-exercises/.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 | -------------------------------------------------------------------------------- /part_9/react-exercises/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 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 start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 35 | 36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 39 | 40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | -------------------------------------------------------------------------------- /part_9/react-exercises/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-exercises", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.11.9", 7 | "@testing-library/react": "^11.2.5", 8 | "@testing-library/user-event": "^12.8.3", 9 | "@types/jest": "^26.0.21", 10 | "@types/node": "^12.20.6", 11 | "@types/react": "^17.0.3", 12 | "@types/react-dom": "^17.0.2", 13 | "react": "^17.0.1", 14 | "react-dom": "^17.0.1", 15 | "react-scripts": "4.0.3", 16 | "typescript": "^4.2.3", 17 | "web-vitals": "^1.1.1" 18 | }, 19 | "scripts": { 20 | "start": "react-scripts start", 21 | "build": "react-scripts build", 22 | "test": "react-scripts test", 23 | "eject": "react-scripts eject", 24 | "lint": "eslint './src/**/*.{ts,tsx}'" 25 | 26 | }, 27 | "eslintConfig": { 28 | "extends": [ 29 | "react-app", 30 | "react-app/jest" 31 | ] 32 | }, 33 | "browserslist": { 34 | "production": [ 35 | ">0.2%", 36 | "not dead", 37 | "not op_mini all" 38 | ], 39 | "development": [ 40 | "last 1 chrome version", 41 | "last 1 firefox version", 42 | "last 1 safari version" 43 | ] 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /part_9/react-exercises/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/villeheikkila/fullstackopen/7d62b2885996e6e68a96fb4cf0133fb371095767/part_9/react-exercises/public/favicon.ico -------------------------------------------------------------------------------- /part_9/react-exercises/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
    32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /part_9/react-exercises/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/villeheikkila/fullstackopen/7d62b2885996e6e68a96fb4cf0133fb371095767/part_9/react-exercises/public/logo192.png -------------------------------------------------------------------------------- /part_9/react-exercises/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/villeheikkila/fullstackopen/7d62b2885996e6e68a96fb4cf0133fb371095767/part_9/react-exercises/public/logo512.png -------------------------------------------------------------------------------- /part_9/react-exercises/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 | -------------------------------------------------------------------------------- /part_9/react-exercises/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /part_9/react-exercises/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./App"; 4 | 5 | 6 | ReactDOM.render(, document.getElementById("root")); -------------------------------------------------------------------------------- /part_9/react-exercises/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /part_9/react-exercises/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | --------------------------------------------------------------------------------