├── .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 | You need to enable JavaScript to run this app.
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 | {text}
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 |
34 |
35 |
36 | Anecdote with most votes
37 | {props.anecdotes[mostVoted]}
38 |
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 |
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 | You need to enable JavaScript to run this app.
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 |
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 | You need to enable JavaScript to run this app.
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 |
33 | {text}
34 |
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 |
60 |
61 |
62 |
statistiikka
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
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 | You need to enable JavaScript to run this app.
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 | You need to enable JavaScript to run this app.
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 |
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 |
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} Poista
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 | You need to enable JavaScript to run this app.
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 |
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 |
8 | Show
9 |
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 |
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 |
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 You need to enable JavaScript to run this 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 | You need to enable JavaScript to run this app.
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 | like
59 |
60 |
61 | added by {blog.author}
62 | remove
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 |
11 | like
12 |
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 | {props.buttonLabel}
24 |
25 |
26 | {props.children}
27 | cancel
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 | You need to enable JavaScript to run this app.
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 |
63 | {notes.map(n => (
64 |
{n.content}
65 | ))}
66 |
67 |
persons
68 |
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 | You need to enable JavaScript to run this app.
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 |
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 | voteId(anecdote.content, anecdote.id)}>vote
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 | You need to enable JavaScript to run this app.
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 |
hyvä
33 |
neutraali
34 |
huono
35 |
nollaa tilastot
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 | You need to enable JavaScript to run this app.
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
likeBlog(blog)}>like
35 |
added by {blog.user.name}
36 | {blog.user.username === props.user.username ? (
removeBlog(blog)}>remove ) : (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 |
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 | logout
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 |
34 | title:
35 |
36 |
37 |
38 | author:
39 |
40 |
41 |
42 | url:
43 |
44 |
45 | create
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 |
24 | {props.buttonLabel}
25 |
26 |
27 |
28 | {props.children}
29 | cancel
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 | You need to enable JavaScript to run this app.
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 | You need to enable JavaScript to run this app.
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 |
40 | born
41 |
42 |
43 | books
44 |
45 |
46 | {authors.map(a =>
47 |
48 | {a.name}
49 | {a.born}
50 | {a.bookCount}
51 |
52 | )}
53 |
54 |
55 | {props.token !== null ? (
56 |
) : (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 |
setFilter(a)}>{a}
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 |
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 |
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 |
24 | author
25 |
26 |
27 | published
28 |
29 |
30 | {books.filter(book => book.genres.includes(user.favoriteGenre)).map(a =>
31 |
32 | {a.title}
33 | {a.author.name}
34 | {a.published}
35 |
36 | )}
37 |
38 |
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 | You need to enable JavaScript to run this app.
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 |
--------------------------------------------------------------------------------