=> {
5 | return diagnoses;
6 | };
7 |
8 | export default {
9 | getDiagnoses,
10 | };
--------------------------------------------------------------------------------
/part7/blog-list-frontend-extended/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 };
--------------------------------------------------------------------------------
/part4/blog-list-backend/index.js:
--------------------------------------------------------------------------------
1 | const config = require('./utils/config');
2 | const app = require('./app');
3 | const http = require('http');
4 |
5 | const server = http.createServer(app);
6 |
7 | server.listen(config.PORT, () => {
8 | console.log(`Server running on port ${config.PORT}`);
9 | });
--------------------------------------------------------------------------------
/part5/bloglist-frontend/cypress/fixtures/users.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "name": "John Doe",
4 | "username": "JohnDoe123",
5 | "password": "johnspassword"
6 | },
7 | {
8 | "name": "Jane Doe",
9 | "username": "JaneDoe123",
10 | "password": "janespassword"
11 | }
12 | ]
--------------------------------------------------------------------------------
/part3/phonebook-backend/build/static/css/main.b5860f0e.chunk.css:
--------------------------------------------------------------------------------
1 | .notification{background:#d3d3d3;font-size:20px;border-style:solid;border-radius:5px;padding:10px;margin-bottom:10px}.notification__failure{color:red}.notification__success{color:green}
2 | /*# sourceMappingURL=main.b5860f0e.chunk.css.map */
--------------------------------------------------------------------------------
/part7/blog-list-backend-extended/index.js:
--------------------------------------------------------------------------------
1 | const config = require('./utils/config');
2 | const app = require('./app');
3 | const http = require('http');
4 |
5 | const server = http.createServer(app);
6 |
7 | server.listen(config.PORT, () => {
8 | console.log(`Server running on port ${config.PORT}`);
9 | });
--------------------------------------------------------------------------------
/part7/blog-list-frontend-extended/cypress/fixtures/users.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "name": "John Doe",
4 | "username": "JohnDoe123",
5 | "password": "johnspassword"
6 | },
7 | {
8 | "name": "Jane Doe",
9 | "username": "JaneDoe123",
10 | "password": "janespassword"
11 | }
12 | ]
--------------------------------------------------------------------------------
/part4/blog-list-backend/utils/config.js:
--------------------------------------------------------------------------------
1 | require('dotenv').config();
2 |
3 | const PORT = process.env.PORT;
4 |
5 | const MONGODB_URI = process.env.NODE_ENV === 'test'
6 | ? process.env.TEST_MONGODB_URI
7 | : process.env.MONGODB_URI;
8 |
9 | module.exports = {
10 | MONGODB_URI,
11 | PORT
12 | };
13 |
--------------------------------------------------------------------------------
/part7/routed-anecdotes/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 | import { BrowserRouter as Router } from 'react-router-dom';
5 |
6 | ReactDOM.render(
7 |
8 |
9 | ,
10 | document.getElementById('root')
11 | );
--------------------------------------------------------------------------------
/part9/patientor-backend/src/routes/diagnoses.ts:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import diagnosisService from '../services/diagnosisService';
3 |
4 | const router = express.Router();
5 |
6 | router.get('/', (_req, res) => {
7 | res.json(diagnosisService.getDiagnoses());
8 | });
9 |
10 | export default router;
--------------------------------------------------------------------------------
/part2/courseinfo2/src/components/Total.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Total = ({ course: { parts } }) => {
4 | const total = parts.reduce((sum, part) => sum + part.exercises, 0);
5 | return (
6 | total of {total} exercises
7 | );
8 | }
9 |
10 | export default Total;
--------------------------------------------------------------------------------
/part7/blog-list-backend-extended/utils/config.js:
--------------------------------------------------------------------------------
1 | require('dotenv').config();
2 |
3 | const PORT = process.env.PORT;
4 |
5 | const MONGODB_URI = process.env.NODE_ENV === 'test'
6 | ? process.env.TEST_MONGODB_URI
7 | : process.env.MONGODB_URI;
8 |
9 | module.exports = {
10 | MONGODB_URI,
11 | PORT
12 | };
13 |
--------------------------------------------------------------------------------
/part4/blog-list-backend/.env.example:
--------------------------------------------------------------------------------
1 | # .env.example
2 |
3 | MONGODB_URI='mongodb+srv://[username:password@]host1[:port1][,...hostN[:portN]][/[defaultauthdb][?options]]'
4 | PORT=3003
5 | SECRET='secretfortokencreationhere'
6 |
7 | TEST_MONGODB_URI='mongodb+srv://[username:password@]host1[:port1][,...hostN[:portN]][/[defaultauthdb][?options]]'
--------------------------------------------------------------------------------
/part5/bloglist-frontend/src/index.css:
--------------------------------------------------------------------------------
1 | .notification {
2 | background: lightgrey;
3 | font-size: 20px;
4 | border-style: solid;
5 | border-radius: 5px;
6 | padding: 10px;
7 | margin-bottom: 10px;
8 | }
9 |
10 | .notification--failure {
11 | color: red;
12 | }
13 |
14 | .notification--success {
15 | color: green;
16 | }
17 |
--------------------------------------------------------------------------------
/part6/redux-anecdotes/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { Provider } from 'react-redux';
4 | import store from './store';
5 | import App from './App';
6 |
7 | ReactDOM.render(
8 |
9 |
10 | ,
11 | document.getElementById('root')
12 | );
--------------------------------------------------------------------------------
/part7/blog-list-backend-extended/.env.example:
--------------------------------------------------------------------------------
1 | # .env.example
2 |
3 | MONGODB_URI='mongodb+srv://[username:password@]host1[:port1][,...hostN[:portN]][/[defaultauthdb][?options]]'
4 | PORT=3003
5 | SECRET='secretfortokencreationhere'
6 |
7 | TEST_MONGODB_URI='mongodb+srv://[username:password@]host1[:port1][,...hostN[:portN]][/[defaultauthdb][?options]]'
--------------------------------------------------------------------------------
/part2/phonebook/src/index.css:
--------------------------------------------------------------------------------
1 | .notification {
2 | background: lightgrey;
3 | font-size: 20px;
4 | border-style: solid;
5 | border-radius: 5px;
6 | padding: 10px;
7 | margin-bottom: 10px;
8 | }
9 |
10 | .notification__failure {
11 | color: red;
12 | }
13 |
14 | .notification__success {
15 | color: green;
16 | }
17 |
18 |
--------------------------------------------------------------------------------
/part4/blog-list-backend/utils/logger.js:
--------------------------------------------------------------------------------
1 | const info = (...params) => {
2 | if (process.env.NODE_ENV !== 'test') {
3 | console.log(...params);
4 | }
5 | };
6 |
7 | const error = (...params) => {
8 | if (process.env.NODE_ENV !== 'test') {
9 | console.error(...params);
10 | }
11 | };
12 |
13 | module.exports = {
14 | info,
15 | error
16 | };
--------------------------------------------------------------------------------
/part7/blog-list-backend-extended/utils/logger.js:
--------------------------------------------------------------------------------
1 | const info = (...params) => {
2 | if (process.env.NODE_ENV !== 'test') {
3 | console.log(...params);
4 | }
5 | };
6 |
7 | const error = (...params) => {
8 | if (process.env.NODE_ENV !== 'test') {
9 | console.error(...params);
10 | }
11 | };
12 |
13 | module.exports = {
14 | info,
15 | error
16 | };
--------------------------------------------------------------------------------
/part2/phonebook/src/components/Person.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Person = ({ person, handleDelete }) => {
4 |
5 | return (
6 |
7 | {person.name} {person.number}
8 | handleDelete(person)}>delete
9 |
10 | )
11 | }
12 |
13 | export default Person;
--------------------------------------------------------------------------------
/part9/patientor-backend/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES6",
4 | "outDir": "./build/",
5 | "module": "commonjs",
6 | "strict": true,
7 | "noUnusedLocals": true,
8 | "noUnusedParameters": true,
9 | "noImplicitReturns": true,
10 | "noFallthroughCasesInSwitch": true,
11 | "esModuleInterop": true
12 | }
13 | }
--------------------------------------------------------------------------------
/part2/countries/src/components/Search.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Search = ({ query, handleQuery }) => {
4 | return (
5 |
6 | Find countries:
7 |
8 |
9 | );
10 | }
11 |
12 | export default Search;
--------------------------------------------------------------------------------
/part9/patientor-frontend/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import 'semantic-ui-css/semantic.min.css';
4 | import App from './App';
5 | import { reducer, StateProvider } from "./state";
6 |
7 | ReactDOM.render(
8 |
9 |
10 | ,
11 | document.getElementById('root')
12 | );
13 |
--------------------------------------------------------------------------------
/part6/redux-anecdotes/src/reducers/filterReducer.js:
--------------------------------------------------------------------------------
1 | const reducer = (state = '', action) => {
2 | switch (action.type) {
3 | case 'FILTER':
4 | return action.payload;
5 | default:
6 | return state;
7 | }
8 | };
9 |
10 | export const setFilter = (filter) => {
11 | return {
12 | type: 'FILTER',
13 | payload: filter
14 | };
15 | };
16 |
17 | export default reducer;
--------------------------------------------------------------------------------
/part2/phonebook/src/components/Search.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Search = ({ query, handleQuery }) => {
4 | return (
5 |
6 |
Search
7 | search:
8 |
12 |
13 | );
14 | }
15 |
16 | export default Search;
--------------------------------------------------------------------------------
/part4/blog-list-backend/controllers/testing.js:
--------------------------------------------------------------------------------
1 | const testingRouter = require('express').Router();
2 | const Note = require('../models/blog');
3 | const User = require('../models/user');
4 |
5 | testingRouter.post('/reset', async (request, response) => {
6 | await Note.deleteMany({});
7 | await User.deleteMany({});
8 |
9 | response.status(204).end();
10 | });
11 |
12 | module.exports = testingRouter;
--------------------------------------------------------------------------------
/part7/blog-list-backend-extended/controllers/testing.js:
--------------------------------------------------------------------------------
1 | const testingRouter = require('express').Router();
2 | const Note = require('../models/blog');
3 | const User = require('../models/user');
4 |
5 | testingRouter.post('/reset', async (request, response) => {
6 | await Note.deleteMany({});
7 | await User.deleteMany({});
8 |
9 | response.status(204).end();
10 | });
11 |
12 | module.exports = testingRouter;
--------------------------------------------------------------------------------
/part3/phonebook-backend/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # misc
12 | .DS_Store
13 | .env
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 |
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 |
--------------------------------------------------------------------------------
/part1/anecdotes/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/part1/courseinfo/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/part1/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 |
--------------------------------------------------------------------------------
/part2/countries/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/part2/courseinfo2/src/components/Content.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Part from './Part';
3 |
4 | const Content = ({ course: { parts } }) => {
5 | return (
6 |
7 | {parts.map(part =>
8 |
12 | )}
13 |
14 | );
15 | }
16 |
17 | export default Content;
--------------------------------------------------------------------------------
/part2/phonebook/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/part7/routed-anecdotes/src/hooks/index.js:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 |
3 | export const useField = (name, type = 'text') => {
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 | const props = { type, name, value, onChange };
15 |
16 | return [ props, reset ];
17 | };
--------------------------------------------------------------------------------
/part2/courseinfo2/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/part6/redux-anecdotes/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/part6/unicafe-redux/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/part7/country-hook/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/part7/ultimate-hooks/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/part9/bmi/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 | }
--------------------------------------------------------------------------------
/part1/anecdotes/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/part1/courseinfo/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/part1/unicafe/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/part2/courseinfo2/src/components/Course.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Header from './Header';
3 | import Content from './Content';
4 | import Total from './Total';
5 |
6 | const Course = ({ course }) => {
7 | return (
8 |
9 |
10 |
11 |
12 |
13 | );
14 | }
15 |
16 | export default Course;
--------------------------------------------------------------------------------
/part2/courseinfo2/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/part7/routed-anecdotes/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/part8/library-frontend/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/part2/phonebook/src/components/Notification.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import '../index.css';
3 |
4 | const Notification = ({ notification }) => {
5 | if (notification === null) {
6 | return null
7 | }
8 |
9 | const className = `notification notification__${notification.type}`
10 |
11 | return (
12 | {notification.message}
13 | );
14 | }
15 |
16 | export default Notification;
--------------------------------------------------------------------------------
/part9/courseinfo-typescript/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/part5/bloglist-frontend/.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 | .eslintcache
--------------------------------------------------------------------------------
/part7/blog-list-frontend-extended/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { Provider } from 'react-redux';
4 | import { BrowserRouter as Router } from 'react-router-dom';
5 | import store from './store';
6 | import App from './App';
7 |
8 | ReactDOM.render(
9 |
10 |
11 |
12 |
13 | ,
14 | document.getElementById('root')
15 | );
--------------------------------------------------------------------------------
/part8/library-backend/models/author.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 | const uniqueValidator = require('mongoose-unique-validator');
3 |
4 | const schema = new mongoose.Schema({
5 | name: {
6 | type: String,
7 | required: true,
8 | unique: true,
9 | minlength: 4
10 | },
11 | born: {
12 | type: Number,
13 | },
14 | });
15 |
16 | schema.plugin(uniqueValidator);
17 |
18 | module.exports = mongoose.model('Author', schema);
--------------------------------------------------------------------------------
/part7/blog-list-frontend-extended/.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 | .eslintcache
--------------------------------------------------------------------------------
/part8/library-backend/models/user.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 | const uniqueValidator = require('mongoose-unique-validator');
3 |
4 | const schema = new mongoose.Schema({
5 | username: {
6 | type: String,
7 | required: true,
8 | unique: true,
9 | minlength: 4
10 | },
11 | favoriteGenre: {
12 | type: String,
13 | }
14 | });
15 |
16 | schema.plugin(uniqueValidator);
17 |
18 | module.exports = mongoose.model('User', schema);
--------------------------------------------------------------------------------
/part7/routed-anecdotes/src/components/Footer.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Footer = () => (
4 |
9 | );
10 |
11 | export default Footer;
--------------------------------------------------------------------------------
/part7/routed-anecdotes/src/components/Menu.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router-dom';
3 |
4 | const Menu = () => {
5 | const padding = {
6 | paddingRight: 5
7 | };
8 |
9 | return (
10 |
11 | anecdotes
12 | create new
13 | about
14 |
15 | );
16 | };
17 |
18 | export default Menu;
--------------------------------------------------------------------------------
/part7/ultimate-hooks/db.json:
--------------------------------------------------------------------------------
1 | {
2 | "notes": [
3 | {
4 | "content": "custom-hooks are grat",
5 | "id": 1
6 | },
7 | {
8 | "content": "the best featrure ever <3",
9 | "id": 2
10 | }
11 | ],
12 | "persons": [
13 | {
14 | "name": "mluukkai",
15 | "number": "040-5483923",
16 | "id": 1
17 | },
18 | {
19 | "name": "kaltsoon",
20 | "number": "045-2323456",
21 | "id": 2
22 | }
23 | ]
24 | }
--------------------------------------------------------------------------------
/part9/courseinfo-typescript/src/components/Total.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { CoursePart } from '../types';
3 |
4 | interface TotalProps {
5 | courseParts: CoursePart[];
6 | }
7 |
8 | const Total = ({ courseParts }: TotalProps) => {
9 | return (
10 |
11 |
12 | Number of exercises{' '}
13 | {courseParts.reduce((carry, part) => carry + part.exerciseCount, 0)}
14 |
15 |
16 | )
17 | };
18 |
19 | export default Total;
--------------------------------------------------------------------------------
/part2/phonebook/db.json:
--------------------------------------------------------------------------------
1 | {
2 | "persons": [
3 | {
4 | "name": "Arto Hellas",
5 | "number": "040-123456",
6 | "id": 1
7 | },
8 | {
9 | "name": "Ada Lovelace",
10 | "number": "39-44-5323523",
11 | "id": 2
12 | },
13 | {
14 | "name": "Dan Abramov",
15 | "number": "12-43-234345",
16 | "id": 3
17 | },
18 | {
19 | "name": "Mary Poppendieck",
20 | "number": "39-23-6423122",
21 | "id": 4
22 | }
23 | ]
24 | }
--------------------------------------------------------------------------------
/part5/bloglist-frontend/cypress/fixtures/blogs.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "title": "First Blog",
4 | "author": "Test Author",
5 | "url": "http://www.example.com/blog1",
6 | "likes": 5
7 | },
8 | {
9 | "title": "Second Blog",
10 | "author": "Test Author",
11 | "url": "http://www.example.com/blog2",
12 | "likes": 1
13 | },
14 | {
15 | "title": "Third Blog",
16 | "author": "Test Author",
17 | "url": "http://www.example.com/blog3",
18 | "likes": 3
19 | }
20 | ]
--------------------------------------------------------------------------------
/part5/bloglist-frontend/src/utils/handleError.js:
--------------------------------------------------------------------------------
1 | const handleError = (error) => {
2 | if (error.response) {
3 | console.error(error.response);
4 |
5 | return error.response.data.error
6 | ? error.response.data.error
7 | : error.message;
8 | } else if (error.request) {
9 | console.error(error.request);
10 | return error.message;
11 | } else {
12 | console.log('Error', error.message);
13 | return error.message;
14 | }
15 | };
16 |
17 | export default handleError;
--------------------------------------------------------------------------------
/part7/blog-list-frontend-extended/cypress/fixtures/blogs.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "title": "First Blog",
4 | "author": "Test Author",
5 | "url": "http://www.example.com/blog1",
6 | "likes": 5
7 | },
8 | {
9 | "title": "Second Blog",
10 | "author": "Test Author",
11 | "url": "http://www.example.com/blog2",
12 | "likes": 1
13 | },
14 | {
15 | "title": "Third Blog",
16 | "author": "Test Author",
17 | "url": "http://www.example.com/blog3",
18 | "likes": 3
19 | }
20 | ]
--------------------------------------------------------------------------------
/part7/blog-list-frontend-extended/src/utils/handleError.js:
--------------------------------------------------------------------------------
1 | const handleError = (error) => {
2 | if (error.response) {
3 | console.error(error.response);
4 |
5 | return error.response.data.error
6 | ? error.response.data.error
7 | : error.message;
8 | } else if (error.request) {
9 | console.error(error.request);
10 | return error.message;
11 | } else {
12 | console.error('Error', error.message);
13 | return error.message;
14 | }
15 | };
16 |
17 | export default handleError;
--------------------------------------------------------------------------------
/part8/library-backend/loaders.js:
--------------------------------------------------------------------------------
1 | const Book = require('./models/book');
2 |
3 | const batchBookCounts = async (authorIds) => {
4 | const books = await Book.find({});
5 |
6 | const bookCounts = books.reduce((bookCounts, book) => {
7 | const authorId = book.author;
8 |
9 | bookCounts[authorId] = (bookCounts[authorId] || 0) + 1;
10 |
11 | return bookCounts;
12 | }, {});
13 |
14 | return authorIds.map((authorId) => bookCounts[authorId] || 0);
15 | };
16 |
17 | module.exports = { batchBookCounts };
--------------------------------------------------------------------------------
/part9/courseinfo-typescript/src/components/Content.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { CoursePart } from '../types';
3 | import Part from './Part';
4 |
5 | interface ContentProps {
6 | courseParts: CoursePart[];
7 | }
8 |
9 | const Content = ({ courseParts }: ContentProps) => {
10 | return (
11 |
12 | {courseParts.map((coursePart) => {
13 | return (
14 |
15 | )
16 | })}
17 |
18 | );
19 | };
20 |
21 | export default Content;
--------------------------------------------------------------------------------
/part7/routed-anecdotes/src/components/Anecdote.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Anecdote = ({ anecdote, voteAnecdote }) => {
4 | if (!anecdote) {
5 | return null;
6 | }
7 |
8 | return (
9 |
10 |
{anecdote.content}
11 |
has {anecdote.votes} votes
12 |
voteAnecdote(anecdote.id)}>
13 | vote
14 |
15 |
for more info see {anecdote.info}
16 |
17 | );
18 | };
19 |
20 | export default Anecdote;
--------------------------------------------------------------------------------
/part7/routed-anecdotes/src/components/AnecdoteList.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router-dom';
3 |
4 | const AnecdoteList = ({ anecdotes }) => (
5 |
6 |
Anecdotes
7 |
8 | {anecdotes.map(anecdote => {
9 | return (
10 |
11 |
12 | {anecdote.content}
13 |
14 |
15 | );
16 | })}
17 |
18 |
19 | );
20 |
21 | export default AnecdoteList;
--------------------------------------------------------------------------------
/part2/phonebook/src/components/PersonList.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Person from './Person'
3 |
4 | const PersonList = ({ filteredPersons, handleDelete }) => {
5 |
6 | return (
7 |
8 |
Numbers
9 | {filteredPersons.map(person =>
10 |
15 | )}
16 |
17 | )
18 | }
19 |
20 | export default PersonList;
--------------------------------------------------------------------------------
/part6/redux-anecdotes/src/components/Filter.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 | import { setFilter } from '../reducers/filterReducer';
4 |
5 | const Filter = ({ setFilter }) => {
6 | const handleChange = (event) => {
7 | setFilter(event.target.value);
8 | };
9 | const style = {
10 | marginBottom: 10
11 | };
12 |
13 | return (
14 |
15 | filter
16 |
17 | );
18 | };
19 |
20 | export default connect(
21 | null,
22 | { setFilter }
23 | )(Filter);
--------------------------------------------------------------------------------
/part3/phonebook-backend/build/static/css/main.b5860f0e.chunk.css.map:
--------------------------------------------------------------------------------
1 | {"version":3,"sources":["webpack://src/index.css"],"names":[],"mappings":"AAAA,cACI,kBAAqB,CACrB,cAAe,CACf,kBAAmB,CACnB,iBAAkB,CAClB,YAAa,CACb,kBACJ,CAEA,uBACG,SACH,CAEA,uBACI,WACH","file":"main.b5860f0e.chunk.css","sourcesContent":[".notification {\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}\r\n\r\n.notification__failure {\r\n color: red;\r\n}\r\n\r\n.notification__success {\r\n color: green;\r\n }\r\n\r\n"]}
--------------------------------------------------------------------------------
/part6/redux-anecdotes/src/components/Notification.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 |
4 | const Notification = ({ notification }) => {
5 | const style = {
6 | border: 'solid',
7 | padding: 10,
8 | borderWidth: 1
9 | };
10 |
11 | if (!notification) return null;
12 |
13 | return (
14 |
15 | {notification}
16 |
17 | );
18 | };
19 |
20 | const mapStateToProps = (state) => {
21 | return { notification: state.notification };
22 | };
23 |
24 | export default connect(mapStateToProps)(Notification);
--------------------------------------------------------------------------------
/part6/unicafe-redux/src/reducer.js:
--------------------------------------------------------------------------------
1 | const initialState = {
2 | good: 0,
3 | ok: 0,
4 | bad: 0
5 | };
6 |
7 | const counterReducer = (state = initialState, action) => {
8 | console.log(action);
9 | switch (action.type) {
10 | case 'GOOD':
11 | return { ...state, good: state.good + 1 };
12 | case 'OK':
13 | return { ...state, ok: state.ok + 1 };
14 | case 'BAD':
15 | return { ...state, bad: state.bad + 1 };
16 | case 'ZERO':
17 | return initialState;
18 | default:
19 | return state;
20 | }
21 |
22 | };
23 |
24 | export default counterReducer;
--------------------------------------------------------------------------------
/part9/courseinfo-typescript/.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 | }
--------------------------------------------------------------------------------
/part7/blog-list-frontend-extended/src/components/UnauthenticatedRoute.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useSelector } from 'react-redux';
3 | import { Route, Redirect } from 'react-router-dom';
4 |
5 | const UnauthenticatedRoute = ({ component: Component, ...rest }) => {
6 | const isLoggedIn = useSelector((state) => state.authentication.isLoggedIn);
7 |
8 | return (
9 | (
10 | isLoggedIn ?
11 |
12 | :
13 | )} />
14 | );
15 | };
16 |
17 | export default UnauthenticatedRoute;
--------------------------------------------------------------------------------
/part8/library-backend/models/book.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 | const uniqueValidator = require('mongoose-unique-validator');
3 |
4 | const schema = new mongoose.Schema({
5 | title: {
6 | type: String,
7 | required: true,
8 | unique: true,
9 | minlength: 2
10 | },
11 | published: {
12 | type: Number,
13 | },
14 | author: {
15 | type: mongoose.Schema.Types.ObjectId,
16 | ref: 'Author'
17 | },
18 | genres: [
19 | { type: String }
20 | ]
21 | });
22 |
23 | schema.plugin(uniqueValidator);
24 |
25 | module.exports = mongoose.model('Book', schema);
--------------------------------------------------------------------------------
/part6/redux-anecdotes/src/services/anecdotes.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 |
3 | const baseUrl = '/anecdotes';
4 |
5 | const getAll = async () => {
6 | const response = await axios.get(baseUrl);
7 | return response.data;
8 | };
9 |
10 | const createNew = async (anecdote) => {
11 | const response = await axios.post(baseUrl, anecdote);
12 | return response.data;
13 | };
14 |
15 | const update = async (id, newAnecdote) => {
16 | const response = await axios.put(`${baseUrl}/${id}`, newAnecdote);
17 | return response.data;
18 | };
19 |
20 | export default {
21 | getAll,
22 | createNew,
23 | update,
24 | };
--------------------------------------------------------------------------------
/part5/bloglist-frontend/src/components/UserInfo.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | const UserInfo = ({ user, handleLogout }) => (
5 |
6 | {user.name} is logged in
7 | Log out
8 |
9 | );
10 |
11 | UserInfo.propTypes = {
12 | user: PropTypes.shape({
13 | name: PropTypes.string.isRequired,
14 | token: PropTypes.string.isRequired,
15 | username: PropTypes.string.isRequired,
16 | }).isRequired,
17 | handleLogout: PropTypes.func.isRequired,
18 | };
19 |
20 | export default UserInfo;
--------------------------------------------------------------------------------
/part1/unicafe/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/part1/anecdotes/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/part1/courseinfo/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/part2/countries/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/part2/courseinfo2/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/part2/phonebook/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/part6/unicafe-redux/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/part7/country-hook/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/part3/phonebook-backend/build/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/part5/bloglist-frontend/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/part6/redux-anecdotes/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/part7/blog-list-frontend-extended/src/components/Comments.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { v4 as uuidv4 } from 'uuid';
3 | import CommentForm from './CommentForm';
4 | import List from '@material-ui/core/List';
5 | import ListItem from '@material-ui/core/ListItem';
6 |
7 | const Comments = ({ blog }) => {
8 | const comments = blog.comments;
9 |
10 | return (
11 |
12 |
Comments:
13 |
14 | {comments.map((comment) => {comment} )}
15 |
16 |
17 |
18 | );
19 | };
20 |
21 | export default Comments;
--------------------------------------------------------------------------------
/part7/routed-anecdotes/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/part7/ultimate-hooks/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/part8/library-frontend/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/part9/patientor-backend/src/index.ts:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import cors from 'cors';
3 | import diagnosesRouter from './routes/diagnoses';
4 | import patientsRouter from './routes/patients';
5 |
6 | const app = express();
7 |
8 | app.use(express.json());
9 |
10 | app.use(cors());
11 |
12 | const PORT = 3001;
13 |
14 | app.get('/api/ping', (_req, res) => {
15 | console.log('someone pinged here');
16 | res.send('pong');
17 | });
18 |
19 | app.use('/api/diagnoses', diagnosesRouter);
20 | app.use('/api/patients', patientsRouter);
21 |
22 | app.listen(PORT, () => {
23 | console.log(`Server running on port ${PORT}`);
24 | });
--------------------------------------------------------------------------------
/part9/courseinfo-typescript/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/part7/blog-list-frontend-extended/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 |
--------------------------------------------------------------------------------
/part4/blog-list-backend/models/blog.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 |
3 | const blogSchema = new mongoose.Schema({
4 | title: { type: String, required: true },
5 | author: String,
6 | url: { type: String, required: true },
7 | likes: { type: Number, default: 0 },
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.toString();
17 | delete returnedObject._id;
18 | delete returnedObject.__v;
19 | }
20 | });
21 |
22 | module.exports = mongoose.model('Blog', blogSchema);
--------------------------------------------------------------------------------
/part9/courseinfo-typescript/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": false,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "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 |
--------------------------------------------------------------------------------
/part7/blog-list-frontend-extended/README.md:
--------------------------------------------------------------------------------
1 | # blog-list-frontend-extended
2 |
3 | The frontend for a full stack application that was extended from the blog app from parts 4 and 5. It is used to share blog post with friends who can then like or comment on them. Redux is used for state management and React Router for routing. Styling is accomplished with Material-UI.
4 |
5 | ## Screenshots
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/part7/blog-list-backend-extended/models/blog.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 |
3 | const blogSchema = new mongoose.Schema({
4 | title: { type: String, required: true },
5 | author: String,
6 | url: { type: String, required: true },
7 | likes: { type: Number, default: 0 },
8 | comments: [String],
9 | user: {
10 | type: mongoose.Schema.Types.ObjectId,
11 | ref: 'User',
12 | }
13 | });
14 |
15 | blogSchema.set('toJSON', {
16 | transform: (document, returnedObject) => {
17 | returnedObject.id = returnedObject._id.toString();
18 | delete returnedObject._id;
19 | delete returnedObject.__v;
20 | }
21 | });
22 |
23 | module.exports = mongoose.model('Blog', blogSchema);
--------------------------------------------------------------------------------
/part6/redux-anecdotes/src/store.js:
--------------------------------------------------------------------------------
1 | import { createStore, combineReducers, applyMiddleware } from 'redux';
2 | import thunk from 'redux-thunk';
3 | import { composeWithDevTools } from 'redux-devtools-extension';
4 |
5 | import anecdoteReducer from './reducers/anecdoteReducer';
6 | import notificationReducer from './reducers/notificationReducer';
7 | import filterReducer from './reducers/filterReducer';
8 |
9 | const reducer = combineReducers({
10 | anecdotes: anecdoteReducer,
11 | notification: notificationReducer,
12 | filter: filterReducer,
13 | });
14 |
15 | const store = createStore(
16 | reducer,
17 | composeWithDevTools(
18 | applyMiddleware(thunk)
19 | )
20 | );
21 |
22 | export default store;
--------------------------------------------------------------------------------
/part9/patientor-frontend/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "skipLibCheck": true,
10 | "esModuleInterop": true,
11 | "allowSyntheticDefaultImports": true,
12 | "strict": true,
13 | "forceConsistentCasingInFileNames": true,
14 | "module": "esnext",
15 | "moduleResolution": "node",
16 | "resolveJsonModule": true,
17 | "isolatedModules": true,
18 | "noEmit": true,
19 | "jsx": "react-jsx",
20 | "downlevelIteration": true,
21 | "allowJs": true,
22 | "noFallthroughCasesInSwitch": true
23 | },
24 | "include": [
25 | "src"
26 | ]
27 | }
28 |
--------------------------------------------------------------------------------
/part5/bloglist-frontend/src/components/Notification.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import '../index.css';
3 | import PropTypes from 'prop-types';
4 |
5 | const Notification = ({ notification }) => {
6 | if (notification === null) {
7 | return null;
8 | }
9 |
10 | const className = `notification notification--${notification.type}`;
11 |
12 | return (
13 | {notification.message}
14 | );
15 | };
16 |
17 | Notification.propTypes = {
18 | notification: PropTypes.shape({
19 | message: PropTypes.string.isRequired,
20 | type: PropTypes.oneOf([
21 | 'success',
22 | 'failure',
23 | ]).isRequired,
24 | }),
25 | };
26 |
27 | export default Notification;
--------------------------------------------------------------------------------
/part8/library-backend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "library-backend",
3 | "version": "1.0.0",
4 | "description": "Backend for library app",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "nodemon index.js",
8 | "test": "echo \"Error: no test specified\" && exit 1",
9 | "lint": "eslint ."
10 | },
11 | "author": "Ruel Neuman",
12 | "license": "MIT",
13 | "devDependencies": {
14 | "eslint": "^7.26.0",
15 | "nodemon": "^2.0.7"
16 | },
17 | "dependencies": {
18 | "apollo-server": "^2.24.0",
19 | "dataloader": "^2.0.0",
20 | "dotenv": "^9.0.2",
21 | "graphql": "^15.5.0",
22 | "jsonwebtoken": "^8.5.1",
23 | "mongoose": "^5.12.9",
24 | "mongoose-unique-validator": "^2.0.3"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/part7/blog-list-frontend-extended/src/components/Notification.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useSelector } from 'react-redux';
3 | import { Alert } from '@material-ui/lab';
4 | import { makeStyles } from '@material-ui/core/styles';
5 |
6 | const useStyles = makeStyles((theme) => ({
7 | alert: {
8 | margin: theme.spacing(1),
9 | },
10 | }));
11 |
12 | const Notification = () => {
13 | const classes = useStyles();
14 |
15 | const notification = useSelector((state) => state.notification);
16 |
17 | if (!notification || !notification.message) {
18 | return null;
19 | }
20 |
21 | return (
22 | {notification.message}
23 | );
24 | };
25 |
26 | export default Notification;
--------------------------------------------------------------------------------
/part8/library-frontend/src/components/BookTable.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const BookTable = ({ books }) => {
4 | if (!books || books.length <= 0) {
5 | return No books found
;
6 | }
7 |
8 | return (
9 |
10 |
11 |
12 |
13 |
14 | Author
15 |
16 |
17 | Published
18 |
19 |
20 | {books.map((book) =>
21 |
22 | {book.title}
23 | {book.author.name}
24 | {book.published}
25 |
26 | )}
27 |
28 |
29 | );
30 | };
31 |
32 | export default BookTable;
--------------------------------------------------------------------------------
/part2/phonebook/src/services/persons.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 |
3 | const baseUrl = '/api/persons';
4 |
5 | const getAll = () => {
6 | const request = axios.get(baseUrl);
7 | return request.then((response) => response.data);
8 | }
9 |
10 | const create = (newObject) => {
11 | const request = axios.post(baseUrl, newObject);
12 | return request.then((response) => response.data);
13 | }
14 |
15 | const remove = (object) => {
16 | const request = axios.delete(`${baseUrl}/${object.id}`, object);
17 | return request;
18 | }
19 |
20 | const update = (id, newObject) => {
21 | const request = axios.put(`${baseUrl}/${id}`, newObject);
22 | return request.then((response) => response.data);
23 | }
24 |
25 | export default { getAll, create, remove, update };
--------------------------------------------------------------------------------
/part9/patientor-frontend/src/components/HealthRatingBar.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Rating } from 'semantic-ui-react';
3 |
4 | type BarProps = {
5 | rating: number;
6 | showText: boolean;
7 | };
8 |
9 | const HEALTHBAR_TEXTS = [
10 | 'The patient is in great shape',
11 | 'The patient has a low risk of getting sick',
12 | 'The patient has a high risk of getting sick',
13 | 'The patient has a diagnosed condition',
14 | ];
15 |
16 | const HealthRatingBar = ({ rating, showText }: BarProps) => {
17 | return (
18 |
19 | {
}
20 | {showText ?
{HEALTHBAR_TEXTS[rating]}
: null}
21 |
22 | );
23 | };
24 |
25 | export default HealthRatingBar;
26 |
--------------------------------------------------------------------------------
/part5/bloglist-frontend/cypress/support/index.js:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example support/index.js is processed and
3 | // loaded automatically before your test files.
4 | //
5 | // This is a great place to put global configuration and
6 | // behavior that modifies Cypress.
7 | //
8 | // You can change the location of this file or turn off
9 | // automatically serving support files with the
10 | // 'supportFile' configuration option.
11 | //
12 | // You can read more here:
13 | // https://on.cypress.io/configuration
14 | // ***********************************************************
15 |
16 | // Import commands.js using ES2015 syntax:
17 | import './commands';
18 |
19 | // Alternatively you can use CommonJS syntax:
20 | // require('./commands')
21 |
--------------------------------------------------------------------------------
/part7/blog-list-frontend-extended/cypress/support/index.js:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example support/index.js is processed and
3 | // loaded automatically before your test files.
4 | //
5 | // This is a great place to put global configuration and
6 | // behavior that modifies Cypress.
7 | //
8 | // You can change the location of this file or turn off
9 | // automatically serving support files with the
10 | // 'supportFile' configuration option.
11 | //
12 | // You can read more here:
13 | // https://on.cypress.io/configuration
14 | // ***********************************************************
15 |
16 | // Import commands.js using ES2015 syntax:
17 | import './commands';
18 |
19 | // Alternatively you can use CommonJS syntax:
20 | // require('./commands')
21 |
--------------------------------------------------------------------------------
/part7/blog-list-frontend-extended/src/store.js:
--------------------------------------------------------------------------------
1 | import { createStore, combineReducers, applyMiddleware } from 'redux';
2 | import thunk from 'redux-thunk';
3 | import { composeWithDevTools } from 'redux-devtools-extension';
4 |
5 | import notificationReducer from './reducers/notificationReducer';
6 | import blogReducer from './reducers/blogReducer';
7 | import userReducer from './reducers/userReducer';
8 | import authenticationReducer from './reducers/authenticationReducer';
9 |
10 | const reducer = combineReducers({
11 | notification: notificationReducer,
12 | blogs: blogReducer,
13 | users: userReducer,
14 | authentication: authenticationReducer,
15 | });
16 |
17 | const store = createStore(
18 | reducer,
19 | composeWithDevTools(
20 | applyMiddleware(thunk)
21 | )
22 | );
23 |
24 | export default store;
--------------------------------------------------------------------------------
/part8/library-backend/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | 'env': {
3 | 'node': true,
4 | 'commonjs': true,
5 | 'es2021': true,
6 | 'jest': true
7 | },
8 | 'extends': 'eslint:recommended',
9 | 'parserOptions': {
10 | 'ecmaVersion': 12
11 | },
12 | 'rules': {
13 | 'indent': [
14 | 'error',
15 | 2
16 | ],
17 | 'linebreak-style': [
18 | 'error',
19 | 'windows'
20 | ],
21 | 'quotes': [
22 | 'error',
23 | 'single'
24 | ],
25 | 'semi': [
26 | 'error',
27 | 'always'
28 | ],
29 | 'eqeqeq': 'error',
30 | 'no-trailing-spaces': 'error',
31 | 'object-curly-spacing': [
32 | 'error', 'always'
33 | ],
34 | 'arrow-spacing': [
35 | 'error', { 'before': true, 'after': true }
36 | ]
37 | }
38 | };
--------------------------------------------------------------------------------
/part4/blog-list-backend/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | 'env': {
3 | 'node': true,
4 | 'commonjs': true,
5 | 'es2021': true,
6 | 'jest': true
7 | },
8 | 'extends': 'eslint:recommended',
9 | 'parserOptions': {
10 | 'ecmaVersion': 12
11 | },
12 | 'rules': {
13 | 'indent': [
14 | 'error',
15 | 2
16 | ],
17 | 'linebreak-style': [
18 | 'error',
19 | 'windows'
20 | ],
21 | 'quotes': [
22 | 'error',
23 | 'single'
24 | ],
25 | 'semi': [
26 | 'error',
27 | 'always'
28 | ],
29 | 'eqeqeq': 'error',
30 | 'no-trailing-spaces': 'error',
31 | 'object-curly-spacing': [
32 | 'error', 'always'
33 | ],
34 | 'arrow-spacing': [
35 | 'error', { 'before': true, 'after': true }
36 | ]
37 | }
38 | };
39 |
--------------------------------------------------------------------------------
/part6/redux-anecdotes/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { useDispatch } from 'react-redux';
3 | import AnecdoteForm from './components/AnecdoteForm';
4 | import AnecdoteList from './components/AnecdoteList';
5 | import Notification from './components/Notification';
6 | import Filter from './components/Filter';
7 | import { initializeAnecdotes } from './reducers/anecdoteReducer';
8 |
9 |
10 | const App = () => {
11 | const dispatch = useDispatch();
12 |
13 | useEffect(() => {
14 | dispatch(initializeAnecdotes());
15 | }, []);
16 |
17 | return (
18 |
19 |
Anecdotes
20 |
21 |
22 |
23 |
Create New
24 |
25 |
26 | );
27 | };
28 |
29 | export default App;
--------------------------------------------------------------------------------
/part3/phonebook-backend/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | 'env': {
3 | 'node': true,
4 | 'commonjs': true,
5 | 'es2021': true
6 | },
7 | 'extends': 'eslint:recommended',
8 | 'parserOptions': {
9 | 'ecmaVersion': 12
10 | },
11 | 'rules': {
12 | 'indent': [
13 | 'error',
14 | 2
15 | ],
16 | 'linebreak-style': [
17 | 'error',
18 | 'windows'
19 | ],
20 | 'quotes': [
21 | 'error',
22 | 'single'
23 | ],
24 | 'semi': [
25 | 'error',
26 | 'always'
27 | ],
28 | 'eqeqeq': 'error',
29 | 'no-trailing-spaces': 'error',
30 | 'object-curly-spacing': [
31 | 'error', 'always'
32 | ],
33 | 'arrow-spacing': [
34 | 'error', { 'before': true, 'after': true }
35 | ],
36 | 'no-console': 0
37 | }
38 | };
39 |
--------------------------------------------------------------------------------
/part7/blog-list-backend-extended/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | 'env': {
3 | 'node': true,
4 | 'commonjs': true,
5 | 'es2021': true,
6 | 'jest': true
7 | },
8 | 'extends': 'eslint:recommended',
9 | 'parserOptions': {
10 | 'ecmaVersion': 12
11 | },
12 | 'rules': {
13 | 'indent': [
14 | 'error',
15 | 2
16 | ],
17 | 'linebreak-style': [
18 | 'error',
19 | 'windows'
20 | ],
21 | 'quotes': [
22 | 'error',
23 | 'single'
24 | ],
25 | 'semi': [
26 | 'error',
27 | 'always'
28 | ],
29 | 'eqeqeq': 'error',
30 | 'no-trailing-spaces': 'error',
31 | 'object-curly-spacing': [
32 | 'error', 'always'
33 | ],
34 | 'arrow-spacing': [
35 | 'error', { 'before': true, 'after': true }
36 | ]
37 | }
38 | };
39 |
--------------------------------------------------------------------------------
/part9/patientor-frontend/src/AddEntryModal/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Modal, Segment } from 'semantic-ui-react';
3 | import AddEntryForm from './AddEntryForm';
4 | import { NewEntry } from '../types';
5 |
6 | interface Props {
7 | modalOpen: boolean;
8 | onClose: () => void;
9 | onSubmit: (values: NewEntry) => void;
10 | error?: string;
11 | }
12 |
13 | const AddEntryModal = ({ modalOpen, onClose, onSubmit, error }: Props) => (
14 |
15 | Add a new entry
16 |
17 | {error && {`Error: ${error}`} }
18 |
19 |
20 |
21 | );
22 |
23 | export default AddEntryModal;
--------------------------------------------------------------------------------
/part9/patientor-frontend/src/AddPatientModal/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Modal, Segment } from 'semantic-ui-react';
3 | import AddPatientForm, { PatientFormValues } from './AddPatientForm';
4 |
5 | interface Props {
6 | modalOpen: boolean;
7 | onClose: () => void;
8 | onSubmit: (values: PatientFormValues) => void;
9 | error?: string;
10 | }
11 |
12 | const AddPatientModal = ({ modalOpen, onClose, onSubmit, error }: Props) => (
13 |
14 | Add a new patient
15 |
16 | {error && {`Error: ${error}`} }
17 |
18 |
19 |
20 | );
21 |
22 | export default AddPatientModal;
23 |
--------------------------------------------------------------------------------
/part9/patientor-frontend/src/PatientPage/DiagnosisList.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { v1 as uuidv1 } from 'uuid';
3 | import { List, Label } from "semantic-ui-react";
4 |
5 | import { Diagnosis } from "../types";
6 | import { useStateValue } from "../state";
7 |
8 | interface DiagnosisListProps {
9 | diagnosisCodes: Array;
10 | }
11 |
12 | const DiagnosisList = ({ diagnosisCodes }: DiagnosisListProps) => {
13 | const [{ diagnoses }] = useStateValue();
14 |
15 | return (
16 |
17 | {diagnosisCodes.map((code) => {
18 | return (
19 |
20 | {code}
21 | {diagnoses[code]?.name}
22 |
23 | );
24 | })}
25 |
26 | );
27 | };
28 |
29 | export default DiagnosisList;
30 |
--------------------------------------------------------------------------------
/part7/blog-list-frontend-extended/src/components/User.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useSelector } from 'react-redux';
3 | import { useParams } from 'react-router-dom';
4 | import List from '@material-ui/core/List';
5 | import ListItem from '@material-ui/core/ListItem';
6 |
7 | const User = () => {
8 | const id = useParams().id;
9 |
10 | const user = useSelector((state) => {
11 | return state.users.users.find((user) => {
12 | return user.id === id;
13 | });
14 | });
15 |
16 | if (!user) {
17 | return null;
18 | }
19 |
20 | return (
21 |
22 |
{user.name}
23 | Blogs Added:
24 |
25 | {user.blogs.map((blog) => {
26 | return {blog.title} ;
27 | })}
28 |
29 |
30 | );
31 | };
32 |
33 | export default User;
--------------------------------------------------------------------------------
/part7/blog-list-frontend-extended/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
14 |
15 | React App
16 |
17 |
18 | You need to enable JavaScript to run this app.
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/part7/routed-anecdotes/src/components/About.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const About = () => (
4 |
5 |
About anecdote app
6 |
According to Wikipedia:
7 |
8 |
An anecdote is a brief, revealing account of an individual person or an incident.
9 | Occasionally humorous, anecdotes differ from jokes because their primary purpose is not simply to provoke laughter but to reveal a truth more general than the brief tale itself,
10 | such as to characterize a person by delineating a specific quirk or trait, to communicate an abstract idea about a person, place, or thing through the concrete details of a short narrative.
11 | An anecdote is "a story with a point."
12 |
13 |
Software engineering is full of excellent anecdotes, at this app you can find the best and add more.
14 |
15 | );
16 |
17 | export default About;
--------------------------------------------------------------------------------
/part9/bmi/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "bmi",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "calculateBmi": "ts-node bmiCalculator.ts",
9 | "calculateExercises": "ts-node exerciseCalculator.ts",
10 | "start": "ts-node index.ts",
11 | "dev": "ts-node-dev index.ts",
12 | "lint": "eslint --ext .ts ."
13 | },
14 | "author": "Ruel Neuman",
15 | "license": "MIT",
16 | "devDependencies": {
17 | "@types/express": "^4.17.11",
18 | "@types/node": "^15.6.0",
19 | "@typescript-eslint/eslint-plugin": "^4.25.0",
20 | "@typescript-eslint/parser": "^4.25.0",
21 | "eslint": "^7.27.0",
22 | "ts-node": "^10.0.0",
23 | "ts-node-dev": "^1.1.6",
24 | "typescript": "^4.2.4"
25 | },
26 | "dependencies": {
27 | "express": "^4.17.1"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/part4/blog-list-backend/models/user.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 | const uniqueValidator = require('mongoose-unique-validator');
3 |
4 | const userSchema = new mongoose.Schema({
5 | username: {
6 | type: String,
7 | minLength: 3,
8 | unique: true,
9 | required: true,
10 | },
11 | name: String,
12 | passwordHash: {
13 | type: String,
14 | required: true,
15 | },
16 | blogs: [
17 | {
18 | type: mongoose.Schema.Types.ObjectId,
19 | ref: 'Blog',
20 | }
21 | ],
22 | });
23 |
24 | userSchema.plugin(uniqueValidator);
25 |
26 | userSchema.set('toJSON', {
27 | transform: (document, returnedObject) => {
28 | returnedObject.id = returnedObject._id.toString();
29 | delete returnedObject._id;
30 | delete returnedObject.__v;
31 | delete returnedObject.passwordHash;
32 | }
33 | });
34 |
35 | module.exports = mongoose.model('User', userSchema);
--------------------------------------------------------------------------------
/part5/bloglist-frontend/cypress/plugins/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 | ///
3 | // ***********************************************************
4 | // This example plugins/index.js can be used to load plugins
5 | //
6 | // You can change the location of this file or turn off loading
7 | // the plugins file with the 'pluginsFile' configuration option.
8 | //
9 | // You can read more here:
10 | // https://on.cypress.io/plugins-guide
11 | // ***********************************************************
12 |
13 | // This function is called when a project is opened or re-opened (e.g. due to
14 | // the project's config changing)
15 |
16 | /**
17 | * @type {Cypress.PluginConfig}
18 | */
19 | // eslint-disable-next-line no-unused-vars
20 | module.exports = (on, config) => {
21 | // `on` is used to hook into various events Cypress emits
22 | // `config` is the resolved Cypress config
23 | };
24 |
--------------------------------------------------------------------------------
/part9/bmi/.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 | }
--------------------------------------------------------------------------------
/part7/blog-list-backend-extended/models/user.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 | const uniqueValidator = require('mongoose-unique-validator');
3 |
4 | const userSchema = new mongoose.Schema({
5 | username: {
6 | type: String,
7 | minLength: 3,
8 | unique: true,
9 | required: true,
10 | },
11 | name: String,
12 | passwordHash: {
13 | type: String,
14 | required: true,
15 | },
16 | blogs: [
17 | {
18 | type: mongoose.Schema.Types.ObjectId,
19 | ref: 'Blog',
20 | }
21 | ],
22 | });
23 |
24 | userSchema.plugin(uniqueValidator);
25 |
26 | userSchema.set('toJSON', {
27 | transform: (document, returnedObject) => {
28 | returnedObject.id = returnedObject._id.toString();
29 | delete returnedObject._id;
30 | delete returnedObject.__v;
31 | delete returnedObject.passwordHash;
32 | }
33 | });
34 |
35 | module.exports = mongoose.model('User', userSchema);
--------------------------------------------------------------------------------
/part7/blog-list-frontend-extended/cypress/plugins/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 | ///
3 | // ***********************************************************
4 | // This example plugins/index.js can be used to load plugins
5 | //
6 | // You can change the location of this file or turn off loading
7 | // the plugins file with the 'pluginsFile' configuration option.
8 | //
9 | // You can read more here:
10 | // https://on.cypress.io/plugins-guide
11 | // ***********************************************************
12 |
13 | // This function is called when a project is opened or re-opened (e.g. due to
14 | // the project's config changing)
15 |
16 | /**
17 | * @type {Cypress.PluginConfig}
18 | */
19 | // eslint-disable-next-line no-unused-vars
20 | module.exports = (on, config) => {
21 | // `on` is used to hook into various events Cypress emits
22 | // `config` is the resolved Cypress config
23 | };
24 |
--------------------------------------------------------------------------------
/part9/courseinfo-typescript/src/types.ts:
--------------------------------------------------------------------------------
1 | export interface CoursePartBase {
2 | name: string;
3 | exerciseCount: number;
4 | type: string;
5 | }
6 |
7 | export interface CoursePartWithDescription extends CoursePartBase {
8 | description: string;
9 | }
10 |
11 | export interface CourseNormalPart extends CoursePartWithDescription {
12 | type: "normal";
13 | }
14 |
15 | export interface CourseProjectPart extends CoursePartBase {
16 | type: "groupProject";
17 | groupProjectCount: number;
18 | }
19 |
20 | export interface CourseSubmissionPart extends CoursePartWithDescription {
21 | type: "submission";
22 | exerciseSubmissionLink: string;
23 | }
24 |
25 | export interface CourseSpecialPart extends CoursePartWithDescription {
26 | type: "special";
27 | requirements: string[];
28 | }
29 |
30 | export type CoursePart =
31 | | CourseNormalPart
32 | | CourseProjectPart
33 | | CourseSubmissionPart
34 | | CourseSpecialPart;
--------------------------------------------------------------------------------
/part9/patientor-backend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "patientor-backend",
3 | "version": "1.0.0",
4 | "description": "patientor backend for full stack open",
5 | "main": "index.js",
6 | "scripts": {
7 | "dev": "ts-node-dev src/index.ts",
8 | "start": "node build/index.js",
9 | "test": "echo \"Error: no test specified\" && exit 1",
10 | "lint": "eslint --ext .ts .",
11 | "tsc": "tsc"
12 | },
13 | "author": "Ruel Neuman",
14 | "license": "MIT",
15 | "devDependencies": {
16 | "@types/cors": "^2.8.10",
17 | "@types/express": "^4.17.12",
18 | "@types/uuid": "^8.3.0",
19 | "@typescript-eslint/eslint-plugin": "^4.25.0",
20 | "@typescript-eslint/parser": "^4.25.0",
21 | "eslint": "^7.27.0",
22 | "ts-node-dev": "^1.1.6",
23 | "typescript": "^4.3.2"
24 | },
25 | "dependencies": {
26 | "cors": "^2.8.5",
27 | "express": "^4.17.1",
28 | "uuid": "^8.3.2"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/part6/unicafe-redux/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { createStore } from 'redux';
4 | import reducer from './reducer';
5 |
6 | const store = createStore(reducer);
7 |
8 | const App = () => {
9 | return (
10 |
11 |
{ store.dispatch({ type: 'GOOD' }); }}>good
12 |
{ store.dispatch({ type: 'OK' }); }}>neutral
13 |
{ store.dispatch({ type: 'BAD' }); }}>bad
14 |
{ store.dispatch({ type: 'ZERO' }); }}>reset stats
15 |
good {store.getState().good}
16 |
neutral {store.getState().ok}
17 |
bad {store.getState().bad}
18 |
19 | );
20 | };
21 |
22 | const renderApp = () => {
23 | ReactDOM.render( , document.getElementById('root'));
24 | };
25 |
26 | renderApp();
27 | store.subscribe(renderApp);
--------------------------------------------------------------------------------
/part9/patientor-frontend/src/PatientPage/EntryDetails.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { Entry, EntryType } from "../types";
4 | import { assertNever } from "../utils";
5 |
6 | import HospitalEntryDetails from "./HospitalEntryDetails";
7 | import OccupationalHealthcareEntryDetails from "./OccupationalHealthcareEntryDetails";
8 | import HealthCheckEntryDetails from "./HealthCheckEntryDetails";
9 |
10 | interface EntryDetailsProps {
11 | entry: Entry;
12 | }
13 |
14 | const EntryDetails = ({ entry }: EntryDetailsProps) => {
15 | switch (entry.type) {
16 | case EntryType.Hospital:
17 | return ;
18 | case EntryType.OccupationalHealthcare:
19 | return ;
20 | case EntryType.HealthCheck:
21 | return ;
22 | default:
23 | return assertNever(entry);
24 | }
25 | };
26 |
27 | export default EntryDetails;
28 |
--------------------------------------------------------------------------------
/part3/phonebook-backend/build/asset-manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": {
3 | "main.css": "/static/css/main.b5860f0e.chunk.css",
4 | "main.js": "/static/js/main.41b0bf33.chunk.js",
5 | "main.js.map": "/static/js/main.41b0bf33.chunk.js.map",
6 | "runtime-main.js": "/static/js/runtime-main.a212f097.js",
7 | "runtime-main.js.map": "/static/js/runtime-main.a212f097.js.map",
8 | "static/js/2.732f3632.chunk.js": "/static/js/2.732f3632.chunk.js",
9 | "static/js/2.732f3632.chunk.js.map": "/static/js/2.732f3632.chunk.js.map",
10 | "index.html": "/index.html",
11 | "static/css/main.b5860f0e.chunk.css.map": "/static/css/main.b5860f0e.chunk.css.map",
12 | "static/js/2.732f3632.chunk.js.LICENSE.txt": "/static/js/2.732f3632.chunk.js.LICENSE.txt"
13 | },
14 | "entrypoints": [
15 | "static/js/runtime-main.a212f097.js",
16 | "static/js/2.732f3632.chunk.js",
17 | "static/css/main.b5860f0e.chunk.css",
18 | "static/js/main.41b0bf33.chunk.js"
19 | ]
20 | }
--------------------------------------------------------------------------------
/part6/redux-anecdotes/src/reducers/notificationReducer.js:
--------------------------------------------------------------------------------
1 | const reducer = (state = '', action) => {
2 | switch (action.type) {
3 | case 'SHOW_NOTIFICATION':
4 | return action.payload;
5 | case 'HIDE_NOTIFICATION':
6 | return '';
7 | default:
8 | return state;
9 | }
10 | };
11 |
12 | let timeoutID = null;
13 |
14 | export const showNotificationWithTimeout = (message, seconds) => {
15 | return async (dispatch) => {
16 | dispatch(showNotification(message));
17 |
18 | if (timeoutID) {
19 | clearTimeout(timeoutID);
20 | }
21 |
22 | timeoutID = setTimeout(() => {
23 | dispatch(hideNotification());
24 | }, seconds * 1000);
25 | };
26 | };
27 |
28 | export const showNotification = (message) => {
29 | return {
30 | type: 'SHOW_NOTIFICATION',
31 | payload: message
32 | };
33 | };
34 |
35 | export const hideNotification = () => {
36 | return { type: 'HIDE_NOTIFICATION' };
37 | };
38 |
39 | export default reducer;
--------------------------------------------------------------------------------
/part1/unicafe/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "unicafe",
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.7.1",
9 | "react": "^17.0.1",
10 | "react-dom": "^17.0.1",
11 | "react-scripts": "4.0.2",
12 | "web-vitals": "^1.1.0"
13 | },
14 | "scripts": {
15 | "start": "react-scripts start",
16 | "build": "react-scripts build",
17 | "test": "react-scripts test",
18 | "eject": "react-scripts eject"
19 | },
20 | "eslintConfig": {
21 | "extends": [
22 | "react-app",
23 | "react-app/jest"
24 | ]
25 | },
26 | "browserslist": {
27 | "production": [
28 | ">0.2%",
29 | "not dead",
30 | "not op_mini all"
31 | ],
32 | "development": [
33 | "last 1 chrome version",
34 | "last 1 firefox version",
35 | "last 1 safari version"
36 | ]
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/part1/anecdotes/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "anecdotes",
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.7.1",
9 | "react": "^17.0.1",
10 | "react-dom": "^17.0.1",
11 | "react-scripts": "4.0.2",
12 | "web-vitals": "^1.1.0"
13 | },
14 | "scripts": {
15 | "start": "react-scripts start",
16 | "build": "react-scripts build",
17 | "test": "react-scripts test",
18 | "eject": "react-scripts eject"
19 | },
20 | "eslintConfig": {
21 | "extends": [
22 | "react-app",
23 | "react-app/jest"
24 | ]
25 | },
26 | "browserslist": {
27 | "production": [
28 | ">0.2%",
29 | "not dead",
30 | "not op_mini all"
31 | ],
32 | "development": [
33 | "last 1 chrome version",
34 | "last 1 firefox version",
35 | "last 1 safari version"
36 | ]
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/part1/courseinfo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "courseinfo",
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.7.1",
9 | "react": "^17.0.1",
10 | "react-dom": "^17.0.1",
11 | "react-scripts": "4.0.2",
12 | "web-vitals": "^1.1.0"
13 | },
14 | "scripts": {
15 | "start": "react-scripts start",
16 | "build": "react-scripts build",
17 | "test": "react-scripts test",
18 | "eject": "react-scripts eject"
19 | },
20 | "eslintConfig": {
21 | "extends": [
22 | "react-app",
23 | "react-app/jest"
24 | ]
25 | },
26 | "browserslist": {
27 | "production": [
28 | ">0.2%",
29 | "not dead",
30 | "not op_mini all"
31 | ],
32 | "development": [
33 | "last 1 chrome version",
34 | "last 1 firefox version",
35 | "last 1 safari version"
36 | ]
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/part2/courseinfo2/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "courseinfo2",
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.7.1",
9 | "react": "^17.0.1",
10 | "react-dom": "^17.0.1",
11 | "react-scripts": "4.0.2",
12 | "web-vitals": "^1.1.0"
13 | },
14 | "scripts": {
15 | "start": "react-scripts start",
16 | "build": "react-scripts build",
17 | "test": "react-scripts test",
18 | "eject": "react-scripts eject"
19 | },
20 | "eslintConfig": {
21 | "extends": [
22 | "react-app",
23 | "react-app/jest"
24 | ]
25 | },
26 | "browserslist": {
27 | "production": [
28 | ">0.2%",
29 | "not dead",
30 | "not op_mini all"
31 | ],
32 | "development": [
33 | "last 1 chrome version",
34 | "last 1 firefox version",
35 | "last 1 safari version"
36 | ]
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/part9/patientor-backend/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "eslint:recommended",
4 | "plugin:@typescript-eslint/recommended",
5 | "plugin:@typescript-eslint/recommended-requiring-type-checking"
6 | ],
7 | "plugins": ["@typescript-eslint"],
8 | "env": {
9 | "browser": true,
10 | "es6": true,
11 | "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 | }
--------------------------------------------------------------------------------
/part2/countries/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "countries",
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.7.3",
9 | "axios": "^0.21.1",
10 | "react": "^17.0.1",
11 | "react-dom": "^17.0.1",
12 | "react-scripts": "4.0.3",
13 | "web-vitals": "^1.1.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 | },
21 | "eslintConfig": {
22 | "extends": [
23 | "react-app",
24 | "react-app/jest"
25 | ]
26 | },
27 | "browserslist": {
28 | "production": [
29 | ">0.2%",
30 | "not dead",
31 | "not op_mini all"
32 | ],
33 | "development": [
34 | "last 1 chrome version",
35 | "last 1 firefox version",
36 | "last 1 safari version"
37 | ]
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/part4/blog-list-backend/controllers/users.js:
--------------------------------------------------------------------------------
1 | const usersRouter = require('express').Router();
2 | const bcrypt = require('bcrypt');
3 | const User = require('../models/user');
4 |
5 | usersRouter.get('/', async (request, response) => {
6 | const users = await User
7 | .find({})
8 | .populate('blogs', { title: 1, author: 1, url: 1});
9 |
10 | response.json(users);
11 | });
12 |
13 | usersRouter.post('/', async (request, response) => {
14 | const body = request.body;
15 |
16 | if (!body.password || body.password.length < 3) {
17 | return response
18 | .status(400)
19 | .json({ error: 'password must be at least 3 characters' });
20 | }
21 |
22 | const saltRounds = 10;
23 | const passwordHash = await bcrypt.hash(body.password, saltRounds);
24 |
25 | const user = new User({
26 | username: body.username,
27 | name: body.name,
28 | passwordHash,
29 | });
30 |
31 | const savedUser = await user.save();
32 |
33 | response.json(savedUser);
34 | });
35 |
36 | module.exports = usersRouter;
--------------------------------------------------------------------------------
/part2/countries/src/components/Country.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Weather from './Weather';
3 |
4 | const Country = ({ country }) => {
5 |
6 | function numberWithCommas(number) {
7 | return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
8 | }
9 |
10 | return (
11 |
12 |
{country.name}
13 |
Capital: {country.capital}
14 |
Population: {numberWithCommas(country.population)}
15 |
Languages:
16 |
17 | {country.languages.map(language => {language.name} )}
18 |
19 |
20 |
24 |
25 | );
26 | }
27 |
28 |
29 |
30 | export default Country;
--------------------------------------------------------------------------------
/part7/blog-list-backend-extended/controllers/users.js:
--------------------------------------------------------------------------------
1 | const usersRouter = require('express').Router();
2 | const bcrypt = require('bcrypt');
3 | const User = require('../models/user');
4 |
5 | usersRouter.get('/', async (request, response) => {
6 | const users = await User
7 | .find({})
8 | .populate('blogs', { title: 1, author: 1, url: 1});
9 |
10 | response.json(users);
11 | });
12 |
13 | usersRouter.post('/', async (request, response) => {
14 | const body = request.body;
15 |
16 | if (!body.password || body.password.length < 3) {
17 | return response
18 | .status(400)
19 | .json({ error: 'password must be at least 3 characters' });
20 | }
21 |
22 | const saltRounds = 10;
23 | const passwordHash = await bcrypt.hash(body.password, saltRounds);
24 |
25 | const user = new User({
26 | username: body.username,
27 | name: body.name,
28 | passwordHash,
29 | });
30 |
31 | const savedUser = await user.save();
32 |
33 | response.json(savedUser);
34 | });
35 |
36 | module.exports = usersRouter;
--------------------------------------------------------------------------------
/part7/country-hook/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "country-hook",
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.7.1",
9 | "axios": "^0.21.1",
10 | "react": "^17.0.1",
11 | "react-dom": "^17.0.1",
12 | "react-scripts": "4.0.2",
13 | "web-vitals": "^1.1.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 | "lint": "eslint ."
21 | },
22 | "eslintConfig": {
23 | "extends": [
24 | "react-app",
25 | "react-app/jest"
26 | ]
27 | },
28 | "browserslist": {
29 | "production": [
30 | ">0.2%",
31 | "not dead",
32 | "not op_mini all"
33 | ],
34 | "development": [
35 | "last 1 chrome version",
36 | "last 1 firefox version",
37 | "last 1 safari version"
38 | ]
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/part4/blog-list-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 |
11 | const passwordCorrect = (user === null || !body?.password)
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(
27 | userForToken,
28 | process.env.SECRET,
29 | { expiresIn: '1h' }
30 | );
31 |
32 | response
33 | .status(200)
34 | .send({ token, username: user.username, name: user.name });
35 |
36 | });
37 |
38 | module.exports = loginRouter;
--------------------------------------------------------------------------------
/part7/blog-list-backend-extended/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 |
11 | const passwordCorrect = (user === null || !body?.password)
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(
27 | userForToken,
28 | process.env.SECRET,
29 | { expiresIn: '1h' }
30 | );
31 |
32 | response
33 | .status(200)
34 | .send({ token, username: user.username, name: user.name });
35 |
36 | });
37 |
38 | module.exports = loginRouter;
--------------------------------------------------------------------------------
/part9/patientor-frontend/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "eslint:recommended",
4 | "plugin:@typescript-eslint/recommended",
5 | "plugin:react/recommended",
6 | "plugin:@typescript-eslint/recommended-requiring-type-checking"
7 | ],
8 | "plugins": ["@typescript-eslint", "react"],
9 | "env": {
10 | "browser": true,
11 | "es6": true
12 | },
13 | "rules": {
14 | "@typescript-eslint/semi": ["error"],
15 | "@typescript-eslint/explicit-function-return-type": 0,
16 | "@typescript-eslint/explicit-module-boundary-types": 0,
17 | "@typescript-eslint/no-unsafe-member-access": 0,
18 | "@typescript-eslint/no-unused-vars": [
19 | "error", { "argsIgnorePattern": "^_" }
20 | ],
21 | "@typescript-eslint/no-explicit-any": 1,
22 | "no-case-declarations": 0,
23 | "react/prop-types": 0
24 | },
25 | "settings": {
26 | "react": {
27 | "pragma": "React",
28 | "version": "detect"
29 | }
30 | },
31 | "parser": "@typescript-eslint/parser",
32 | "parserOptions": {
33 | "project": "./tsconfig.json"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/part8/library-frontend/src/components/Recommendations.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useQuery } from '@apollo/client';
3 | import { ALL_BOOKS, ME } from '../queries';
4 | import BookTable from './BookTable';
5 |
6 | const Recommendations = () => {
7 | const { data: meData, loading: meLoading, error: meError } = useQuery(ME, {
8 | fetchPolicy: 'cache-and-network',
9 | });
10 |
11 | const genre = meData?.me?.favoriteGenre;
12 |
13 | const { data: booksData, loading: booksLoading, error: booksError } = useQuery(ALL_BOOKS, {
14 | variables: { genre },
15 | skip: !genre,
16 | fetchPolicy: 'cache-and-network',
17 | });
18 |
19 | if (booksLoading || meLoading) {
20 | return loading...
;
21 | }
22 |
23 | if (booksError || meError) {
24 | return Error: Could not load books
;
25 | }
26 |
27 | return (
28 |
29 |
Recommendations
30 |
Your favorite genre: {genre}
31 |
32 |
33 | );
34 | };
35 |
36 | export default Recommendations;
--------------------------------------------------------------------------------
/part9/patientor-frontend/src/state/state.tsx:
--------------------------------------------------------------------------------
1 | import React, { createContext, useContext, useReducer } from "react";
2 | import { Patient, Diagnosis } from "../types";
3 |
4 | import { Action } from "./reducer";
5 |
6 | export type State = {
7 | patients: { [id: string]: Patient };
8 | diagnoses: { [code: string]: Diagnosis };
9 | };
10 |
11 | const initialState: State = {
12 | patients: {},
13 | diagnoses: {},
14 | };
15 |
16 | export const StateContext = createContext<[State, React.Dispatch]>([
17 | initialState,
18 | () => initialState
19 | ]);
20 |
21 | type StateProviderProps = {
22 | reducer: React.Reducer;
23 | children: React.ReactElement;
24 | };
25 |
26 | export const StateProvider: React.FC = ({
27 | reducer,
28 | children
29 | }: StateProviderProps) => {
30 | const [state, dispatch] = useReducer(reducer, initialState);
31 | return (
32 |
33 | {children}
34 |
35 | );
36 | };
37 | export const useStateValue = () => useContext(StateContext);
38 |
--------------------------------------------------------------------------------
/part7/routed-anecdotes/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "routed-anecdotes",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^5.11.9",
7 | "@testing-library/react": "^11.2.5",
8 | "@testing-library/user-event": "^12.7.1",
9 | "react": "^17.0.1",
10 | "react-dom": "^17.0.1",
11 | "react-router": "^5.2.0",
12 | "react-router-dom": "^5.2.0",
13 | "react-scripts": "4.0.2",
14 | "web-vitals": "^1.1.0"
15 | },
16 | "scripts": {
17 | "start": "react-scripts start",
18 | "build": "react-scripts build",
19 | "test": "react-scripts test",
20 | "eject": "react-scripts eject",
21 | "lint": "eslint ."
22 | },
23 | "eslintConfig": {
24 | "extends": [
25 | "react-app",
26 | "react-app/jest"
27 | ]
28 | },
29 | "browserslist": {
30 | "production": [
31 | ">0.2%",
32 | "not dead",
33 | "not op_mini all"
34 | ],
35 | "development": [
36 | "last 1 chrome version",
37 | "last 1 firefox version",
38 | "last 1 safari version"
39 | ]
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/part5/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 getAll = async () => {
11 | const response = await axios.get(baseUrl);
12 | return response.data;
13 | };
14 |
15 | const create = async (newObject) => {
16 | const config = {
17 | headers: { Authorization: token },
18 | };
19 |
20 | const response = await axios.post(baseUrl, newObject, config);
21 | return response.data;
22 | };
23 |
24 | const update = async (id, newObject) => {
25 | const config = {
26 | headers: { Authorization: token },
27 | };
28 |
29 | const response = await axios.put(`${baseUrl}/${id}`, newObject, config);
30 | return response.data;
31 | };
32 |
33 | const remove = async (id) => {
34 | const config = {
35 | headers: { Authorization: token },
36 | };
37 |
38 | const response = await axios.delete(`${baseUrl}/${id}`, config);
39 | return response.data;
40 | };
41 |
42 | export default { getAll, create, update, remove, setToken };
--------------------------------------------------------------------------------
/part4/blog-list-backend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "blog-list-backend",
3 | "version": "1.0.0",
4 | "description": "Blog list backend for Full Stack Open course",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "cross-env NODE_ENV=production node index.js",
8 | "dev": "cross-env NODE_ENV=development nodemon index.js",
9 | "test": "cross-env NODE_ENV=test jest --verbose --runInBand",
10 | "start:test": "cross-env NODE_ENV=test node index.js",
11 | "lint": "eslint . && echo 'Lint complete'"
12 | },
13 | "author": "Ruel Neuman",
14 | "license": "MIT",
15 | "dependencies": {
16 | "bcrypt": "^5.0.1",
17 | "cors": "^2.8.5",
18 | "dotenv": "^8.2.0",
19 | "express": "^4.17.1",
20 | "express-async-errors": "^3.1.1",
21 | "jsonwebtoken": "^8.5.1",
22 | "mongoose": "^5.12.0",
23 | "mongoose-unique-validator": "^2.0.3"
24 | },
25 | "devDependencies": {
26 | "cross-env": "^7.0.3",
27 | "eslint": "^7.22.0",
28 | "jest": "^26.6.3",
29 | "nodemon": "^2.0.7",
30 | "supertest": "^6.1.3"
31 | },
32 | "jest": {
33 | "testEnvironment": "node"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/part8/library-frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "frontend",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@apollo/client": "^3.3.19",
7 | "@testing-library/jest-dom": "^4.2.4",
8 | "@testing-library/react": "^9.4.1",
9 | "@testing-library/user-event": "^7.2.1",
10 | "graphql": "^15.5.0",
11 | "react": "^16.12.0",
12 | "react-dom": "^16.12.0",
13 | "react-scripts": "^4.0.3",
14 | "subscriptions-transport-ws": "^0.9.18"
15 | },
16 | "scripts": {
17 | "start": "react-scripts start",
18 | "build": "react-scripts build",
19 | "test": "react-scripts test",
20 | "eject": "react-scripts eject",
21 | "lint": "eslint ."
22 | },
23 | "eslintConfig": {
24 | "extends": "react-app"
25 | },
26 | "browserslist": {
27 | "production": [
28 | ">0.2%",
29 | "not dead",
30 | "not op_mini all"
31 | ],
32 | "development": [
33 | "last 1 chrome version",
34 | "last 1 firefox version",
35 | "last 1 safari version"
36 | ]
37 | },
38 | "devDependencies": {
39 | "eslint-plugin-jest": "^24.3.6"
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/part5/bloglist-frontend/src/components/Togglable.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | const Togglable = ({ buttonLabel, children }) => {
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 | const childrenWithVisibilityToggle = React.Children.map(children, child => {
15 | return React.cloneElement(child, { toggleVisibility });
16 | });
17 |
18 | return (
19 |
20 |
21 | {buttonLabel}
22 |
23 |
24 | {childrenWithVisibilityToggle}
25 | Cancel
26 |
27 |
28 | );
29 | };
30 |
31 | Togglable.propTypes = {
32 | buttonLabel: PropTypes.string.isRequired,
33 | children: PropTypes.element.isRequired,
34 | };
35 |
36 |
37 | export default Togglable;
--------------------------------------------------------------------------------
/part7/blog-list-backend-extended/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "blog-list-backend-extended",
3 | "version": "1.0.0",
4 | "description": "Blog list backend for Full Stack Open course",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "cross-env NODE_ENV=production node index.js",
8 | "dev": "cross-env NODE_ENV=development nodemon index.js",
9 | "test": "cross-env NODE_ENV=test jest --verbose --runInBand",
10 | "start:test": "cross-env NODE_ENV=test node index.js",
11 | "lint": "eslint . && echo 'Lint complete'"
12 | },
13 | "author": "Ruel Neuman",
14 | "license": "MIT",
15 | "dependencies": {
16 | "bcrypt": "^5.0.1",
17 | "cors": "^2.8.5",
18 | "dotenv": "^8.2.0",
19 | "express": "^4.17.1",
20 | "express-async-errors": "^3.1.1",
21 | "jsonwebtoken": "^8.5.1",
22 | "mongoose": "^5.12.0",
23 | "mongoose-unique-validator": "^2.0.3"
24 | },
25 | "devDependencies": {
26 | "cross-env": "^7.0.3",
27 | "eslint": "^7.22.0",
28 | "jest": "^26.6.3",
29 | "nodemon": "^2.0.7",
30 | "supertest": "^6.1.3"
31 | },
32 | "jest": {
33 | "testEnvironment": "node"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/part3/phonebook-backend/models/person.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 | const uniqueValidator = require('mongoose-unique-validator');
3 |
4 | const url = process.env.MONGODB_URI;
5 |
6 | mongoose
7 | .connect(url, {
8 | useNewUrlParser: true,
9 | useUnifiedTopology: true,
10 | useFindAndModify: false,
11 | useCreateIndex: true
12 | })
13 | .then(() => {
14 | console.log('connected to MongoDB');
15 | })
16 | .catch((error) => {
17 | console.log('error connecting to MongoDB:', error.message);
18 | });
19 |
20 | const personSchema = new mongoose.Schema({
21 | name: {
22 | type: String,
23 | required: true,
24 | unique: true,
25 | minlength: 3,
26 | },
27 | number: {
28 | type: String,
29 | required: true,
30 | minlength: 8,
31 | },
32 | });
33 |
34 | personSchema.plugin(uniqueValidator);
35 |
36 | personSchema.set('toJSON', {
37 | transform: (document, returnedObject) => {
38 | returnedObject.id = returnedObject._id.toString();
39 | delete returnedObject._id;
40 | delete returnedObject.__v;
41 | }
42 | });
43 |
44 | module.exports = mongoose.model('Person', personSchema);
--------------------------------------------------------------------------------
/part6/unicafe-redux/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "unicafe-redux",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^5.11.9",
7 | "@testing-library/react": "^11.2.5",
8 | "@testing-library/user-event": "^12.6.3",
9 | "react": "^17.0.1",
10 | "react-dom": "^17.0.1",
11 | "react-scripts": "4.0.2",
12 | "redux": "^4.0.5",
13 | "web-vitals": "^1.1.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 | "lint": "eslint ."
21 | },
22 | "eslintConfig": {
23 | "extends": [
24 | "react-app",
25 | "react-app/jest"
26 | ]
27 | },
28 | "browserslist": {
29 | "production": [
30 | ">0.2%",
31 | "not dead",
32 | "not op_mini all"
33 | ],
34 | "development": [
35 | "last 1 chrome version",
36 | "last 1 firefox version",
37 | "last 1 safari version"
38 | ]
39 | },
40 | "devDependencies": {
41 | "deep-freeze": "0.0.1",
42 | "eslint-plugin-jest": "^24.3.5"
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/part9/patientor-frontend/src/PatientPage/HealthCheckEntryDetails.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { HealthCheckEntry } from "../types";
3 | import { Card, Icon } from "semantic-ui-react";
4 | import DiagnosisList from "./DiagnosisList";
5 | import HealthRatingBar from "../components/HealthRatingBar";
6 |
7 | interface HealthCheckEntryDetailsProps {
8 | entry: HealthCheckEntry;
9 | }
10 |
11 | const HealthCheckEntryDetails = ({ entry }: HealthCheckEntryDetailsProps) => {
12 | return (
13 |
14 |
15 |
16 | {entry.date}
17 |
18 |
19 | {entry.specialist}
20 | {entry.description}
21 |
22 |
23 |
24 | {entry.diagnosisCodes
25 | ?
26 | : No diagnosis codes
}
27 |
28 |
29 | );
30 | };
31 |
32 | export default HealthCheckEntryDetails;
--------------------------------------------------------------------------------
/part6/redux-anecdotes/db.json:
--------------------------------------------------------------------------------
1 | {
2 | "anecdotes": [
3 | {
4 | "content": "If it hurts, do it more often",
5 | "id": 1,
6 | "votes": 0
7 | },
8 | {
9 | "content": "Adding manpower to a late software project makes it later!",
10 | "id": 2,
11 | "votes": 1
12 | },
13 | {
14 | "content": "The first 90 percent of the code accounts for the first 90 percent of the development time...The remaining 10 percent of the code accounts for the other 90 percent of the development time.",
15 | "id": 3,
16 | "votes": 0
17 | },
18 | {
19 | "content": "Any fool can write code that a computer can understand. Good programmers write code that humans can understand.",
20 | "id": 4,
21 | "votes": 11
22 | },
23 | {
24 | "content": "Premature optimization is the root of all evil.",
25 | "id": 5,
26 | "votes": 0
27 | },
28 | {
29 | "content": "Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it.",
30 | "id": 6,
31 | "votes": 4
32 | }
33 | ]
34 | }
--------------------------------------------------------------------------------
/part2/phonebook/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "phonebook",
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.7.1",
9 | "axios": "^0.21.1",
10 | "react": "^17.0.1",
11 | "react-dom": "^17.0.1",
12 | "react-scripts": "4.0.2",
13 | "web-vitals": "^1.1.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 --watch db.json"
21 | },
22 | "proxy": "http://localhost:3001",
23 | "eslintConfig": {
24 | "extends": [
25 | "react-app",
26 | "react-app/jest"
27 | ]
28 | },
29 | "browserslist": {
30 | "production": [
31 | ">0.2%",
32 | "not dead",
33 | "not op_mini all"
34 | ],
35 | "development": [
36 | "last 1 chrome version",
37 | "last 1 firefox version",
38 | "last 1 safari version"
39 | ]
40 | },
41 | "devDependencies": {
42 | "json-server": "^0.16.3"
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/part7/ultimate-hooks/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ultimate-hooks",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^5.11.9",
7 | "@testing-library/react": "^11.2.5",
8 | "@testing-library/user-event": "^12.7.1",
9 | "axios": "^0.21.1",
10 | "react": "^17.0.1",
11 | "react-dom": "^17.0.1",
12 | "react-scripts": "4.0.2",
13 | "web-vitals": "^1.1.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 --port=3005 --watch db.json",
21 | "lint": "eslint ."
22 | },
23 | "eslintConfig": {
24 | "extends": [
25 | "react-app",
26 | "react-app/jest"
27 | ]
28 | },
29 | "browserslist": {
30 | "production": [
31 | ">0.2%",
32 | "not dead",
33 | "not op_mini all"
34 | ],
35 | "development": [
36 | "last 1 chrome version",
37 | "last 1 firefox version",
38 | "last 1 safari version"
39 | ]
40 | },
41 | "devDependencies": {
42 | "json-server": "^0.16.3"
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/part6/unicafe-redux/.eslintrc.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 | module.exports = {
3 | 'env': {
4 | 'browser': true,
5 | 'es6': true,
6 | 'jest/globals': true
7 | },
8 | 'extends': [
9 | 'eslint:recommended',
10 | 'plugin:react/recommended'
11 | ],
12 | 'parserOptions': {
13 | 'ecmaFeatures': {
14 | 'jsx': true
15 | },
16 | 'ecmaVersion': 2018,
17 | 'sourceType': 'module'
18 | },
19 | 'plugins': [
20 | 'react', 'jest'
21 | ],
22 | 'rules': {
23 | 'indent': [
24 | 'error',
25 | 2,
26 | { 'SwitchCase': 1 }
27 | ],
28 | 'linebreak-style': [
29 | 'error',
30 | 'windows'
31 | ],
32 | 'quotes': [
33 | 'error',
34 | 'single'
35 | ],
36 | 'semi': [
37 | 'error',
38 | 'always'
39 | ],
40 | 'eqeqeq': 'error',
41 | 'no-trailing-spaces': 'error',
42 | 'object-curly-spacing': [
43 | 'error', 'always'
44 | ],
45 | 'arrow-spacing': [
46 | 'error', { 'before': true, 'after': true }
47 | ],
48 | 'no-console': 0,
49 | 'react/prop-types': 0
50 | },
51 | 'settings': {
52 | 'react': {
53 | 'version': 'detect'
54 | }
55 | }
56 | };
--------------------------------------------------------------------------------
/part7/country-hook/.eslintrc.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 | module.exports = {
3 | 'env': {
4 | 'browser': true,
5 | 'es6': true,
6 | 'jest/globals': true
7 | },
8 | 'extends': [
9 | 'eslint:recommended',
10 | 'plugin:react/recommended'
11 | ],
12 | 'parserOptions': {
13 | 'ecmaFeatures': {
14 | 'jsx': true
15 | },
16 | 'ecmaVersion': 2018,
17 | 'sourceType': 'module'
18 | },
19 | 'plugins': [
20 | 'react', 'jest'
21 | ],
22 | 'rules': {
23 | 'indent': [
24 | 'error',
25 | 2,
26 | { 'SwitchCase': 1 }
27 | ],
28 | 'linebreak-style': [
29 | 'error',
30 | 'windows'
31 | ],
32 | 'quotes': [
33 | 'error',
34 | 'single'
35 | ],
36 | 'semi': [
37 | 'error',
38 | 'always'
39 | ],
40 | 'eqeqeq': 'error',
41 | 'no-trailing-spaces': 'error',
42 | 'object-curly-spacing': [
43 | 'error', 'always'
44 | ],
45 | 'arrow-spacing': [
46 | 'error', { 'before': true, 'after': true }
47 | ],
48 | 'no-console': 0,
49 | 'react/prop-types': 0
50 | },
51 | 'settings': {
52 | 'react': {
53 | 'version': 'detect'
54 | }
55 | }
56 | };
--------------------------------------------------------------------------------
/part7/ultimate-hooks/.eslintrc.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 | module.exports = {
3 | 'env': {
4 | 'browser': true,
5 | 'es6': true,
6 | 'jest/globals': true
7 | },
8 | 'extends': [
9 | 'eslint:recommended',
10 | 'plugin:react/recommended'
11 | ],
12 | 'parserOptions': {
13 | 'ecmaFeatures': {
14 | 'jsx': true
15 | },
16 | 'ecmaVersion': 2018,
17 | 'sourceType': 'module'
18 | },
19 | 'plugins': [
20 | 'react', 'jest'
21 | ],
22 | 'rules': {
23 | 'indent': [
24 | 'error',
25 | 2,
26 | { 'SwitchCase': 1 }
27 | ],
28 | 'linebreak-style': [
29 | 'error',
30 | 'windows'
31 | ],
32 | 'quotes': [
33 | 'error',
34 | 'single'
35 | ],
36 | 'semi': [
37 | 'error',
38 | 'always'
39 | ],
40 | 'eqeqeq': 'error',
41 | 'no-trailing-spaces': 'error',
42 | 'object-curly-spacing': [
43 | 'error', 'always'
44 | ],
45 | 'arrow-spacing': [
46 | 'error', { 'before': true, 'after': true }
47 | ],
48 | 'no-console': 0,
49 | 'react/prop-types': 0
50 | },
51 | 'settings': {
52 | 'react': {
53 | 'version': 'detect'
54 | }
55 | }
56 | };
--------------------------------------------------------------------------------
/part6/redux-anecdotes/.eslintrc.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 | module.exports = {
3 | 'env': {
4 | 'browser': true,
5 | 'es6': true,
6 | 'jest/globals': true
7 | },
8 | 'extends': [
9 | 'eslint:recommended',
10 | 'plugin:react/recommended'
11 | ],
12 | 'parserOptions': {
13 | 'ecmaFeatures': {
14 | 'jsx': true
15 | },
16 | 'ecmaVersion': 2018,
17 | 'sourceType': 'module'
18 | },
19 | 'plugins': [
20 | 'react', 'jest'
21 | ],
22 | 'rules': {
23 | 'indent': [
24 | 'error',
25 | 2,
26 | { 'SwitchCase': 1 }
27 | ],
28 | 'linebreak-style': [
29 | 'error',
30 | 'windows'
31 | ],
32 | 'quotes': [
33 | 'error',
34 | 'single'
35 | ],
36 | 'semi': [
37 | 'error',
38 | 'always'
39 | ],
40 | 'eqeqeq': 'error',
41 | 'no-trailing-spaces': 'error',
42 | 'object-curly-spacing': [
43 | 'error', 'always'
44 | ],
45 | 'arrow-spacing': [
46 | 'error', { 'before': true, 'after': true }
47 | ],
48 | 'no-console': 0,
49 | 'react/prop-types': 0
50 | },
51 | 'settings': {
52 | 'react': {
53 | 'version': 'detect'
54 | }
55 | }
56 | };
--------------------------------------------------------------------------------
/part7/routed-anecdotes/.eslintrc.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 | module.exports = {
3 | 'env': {
4 | 'browser': true,
5 | 'es6': true,
6 | 'jest/globals': true
7 | },
8 | 'extends': [
9 | 'eslint:recommended',
10 | 'plugin:react/recommended'
11 | ],
12 | 'parserOptions': {
13 | 'ecmaFeatures': {
14 | 'jsx': true
15 | },
16 | 'ecmaVersion': 2018,
17 | 'sourceType': 'module'
18 | },
19 | 'plugins': [
20 | 'react', 'jest'
21 | ],
22 | 'rules': {
23 | 'indent': [
24 | 'error',
25 | 2,
26 | { 'SwitchCase': 1 }
27 | ],
28 | 'linebreak-style': [
29 | 'error',
30 | 'windows'
31 | ],
32 | 'quotes': [
33 | 'error',
34 | 'single'
35 | ],
36 | 'semi': [
37 | 'error',
38 | 'always'
39 | ],
40 | 'eqeqeq': 'error',
41 | 'no-trailing-spaces': 'error',
42 | 'object-curly-spacing': [
43 | 'error', 'always'
44 | ],
45 | 'arrow-spacing': [
46 | 'error', { 'before': true, 'after': true }
47 | ],
48 | 'no-console': 0,
49 | 'react/prop-types': 0
50 | },
51 | 'settings': {
52 | 'react': {
53 | 'version': 'detect'
54 | }
55 | }
56 | };
--------------------------------------------------------------------------------
/part3/phonebook-backend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "phonebook-backend",
3 | "version": "1.0.0",
4 | "description": "Backend for the phonebook app",
5 | "main": "index.js",
6 | "private": true,
7 | "scripts": {
8 | "start": "node index.js",
9 | "dev": "nodemon index.js",
10 | "test": "echo \"Error: no test specified\" && exit 1",
11 | "build:ui": "rm -rf build && cd ../../part2/phonebook && npm run build --prod && cp -r build ../../part3/phonebook-backend/",
12 | "deploy": "cd ../.. && git subtree push --prefix part3/phonebook-backend heroku master && cd part3/phonebook-backend/",
13 | "deploy:full": "npm run build:ui && cd ../.. && git add . && git commit -m uibuild && cd part3/phonebook-backend/ && npm run deploy",
14 | "logs:prod": "heroku logs --tail",
15 | "lint": "eslint ."
16 | },
17 | "author": "Ruel Neuman",
18 | "license": "MIT",
19 | "devDependencies": {
20 | "eslint": "^7.22.0",
21 | "nodemon": "^2.0.7"
22 | },
23 | "dependencies": {
24 | "cors": "^2.8.5",
25 | "dotenv": "^8.2.0",
26 | "express": "^4.17.1",
27 | "mongoose": "^5.11.19",
28 | "mongoose-unique-validator": "^2.0.3",
29 | "morgan": "^1.10.0"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/part9/patientor-frontend/src/PatientPage/HospitalEntryDetails.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { HospitalEntry } from "../types";
3 | import { Card, Icon } from "semantic-ui-react";
4 | import DiagnosisList from "./DiagnosisList";
5 |
6 | interface HospitalEntryDetailsProps {
7 | entry: HospitalEntry;
8 | }
9 |
10 | const HospitalEntryDetails = ({ entry }: HospitalEntryDetailsProps) => {
11 | return (
12 |
13 |
14 |
15 | {entry.date}
16 |
17 |
18 | {entry.specialist}
19 |
20 | {entry.description}
21 | Discharge Date: {entry.discharge.date}
22 | Discharge Criteria: {entry.discharge.criteria}
23 |
24 |
25 |
26 | {entry.diagnosisCodes
27 | ?
28 | : No diagnosis codes
}
29 |
30 |
31 | );
32 | };
33 |
34 | export default HospitalEntryDetails;
--------------------------------------------------------------------------------
/part8/library-frontend/.eslintrc.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 |
3 | module.exports = {
4 | 'env': {
5 | 'browser': true,
6 | 'es6': true,
7 | 'jest/globals': true
8 | },
9 | 'extends': [
10 | 'eslint:recommended',
11 | 'plugin:react/recommended',
12 | 'plugin:jest/recommended'
13 | ],
14 | 'parserOptions': {
15 | 'ecmaFeatures': {
16 | 'jsx': true
17 | },
18 | 'ecmaVersion': 2020,
19 | 'sourceType': 'module'
20 | },
21 | 'plugins': [
22 | 'react', 'jest'
23 | ],
24 | 'rules': {
25 | 'indent': [
26 | 'error',
27 | 2,
28 | { 'SwitchCase': 1 }
29 | ],
30 | 'linebreak-style': [
31 | 'error',
32 | 'windows'
33 | ],
34 | 'quotes': [
35 | 'error',
36 | 'single'
37 | ],
38 | 'semi': [
39 | 'error',
40 | 'always'
41 | ],
42 | 'eqeqeq': 'error',
43 | 'no-trailing-spaces': 'error',
44 | 'object-curly-spacing': [
45 | 'error', 'always'
46 | ],
47 | 'arrow-spacing': [
48 | 'error', { 'before': true, 'after': true }
49 | ],
50 | 'no-console': 0,
51 | 'react/prop-types': 0
52 | },
53 | 'settings': {
54 | 'react': {
55 | 'version': 'detect'
56 | }
57 | }
58 | };
--------------------------------------------------------------------------------
/part9/courseinfo-typescript/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "courseinfo-typescript",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^5.12.0",
7 | "@testing-library/react": "^11.2.7",
8 | "@testing-library/user-event": "^12.8.3",
9 | "@types/jest": "^26.0.23",
10 | "@types/node": "^12.20.13",
11 | "@types/react": "^17.0.8",
12 | "@types/react-dom": "^17.0.5",
13 | "react": "^17.0.2",
14 | "react-dom": "^17.0.2",
15 | "react-scripts": "4.0.3",
16 | "typescript": "^4.3.2",
17 | "web-vitals": "^1.1.2"
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 | "eslintConfig": {
27 | "extends": [
28 | "react-app",
29 | "react-app/jest"
30 | ]
31 | },
32 | "browserslist": {
33 | "production": [
34 | ">0.2%",
35 | "not dead",
36 | "not op_mini all"
37 | ],
38 | "development": [
39 | "last 1 chrome version",
40 | "last 1 firefox version",
41 | "last 1 safari version"
42 | ]
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/part8/library-frontend/src/components/Authors.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useQuery } from '@apollo/client';
3 | import { ALL_AUTHORS } from '../queries';
4 | import BirthYearForm from './BirthYearForm';
5 |
6 | const Authors = ({ token }) => {
7 | const { data, loading, error } = useQuery(ALL_AUTHORS);
8 |
9 | if (loading) {
10 | return loading...
;
11 | }
12 |
13 | if (error) {
14 | return Error: Could not load authors
;
15 | }
16 |
17 | const authors = data.allAuthors;
18 |
19 | return (
20 |
21 |
Authors
22 |
23 |
24 |
25 |
26 |
27 | Born
28 |
29 |
30 | Books
31 |
32 |
33 | {authors.map((author) =>
34 |
35 | {author.name}
36 | {author.born}
37 | {author.bookCount}
38 |
39 | )}
40 |
41 |
42 |
43 | {token &&
}
44 |
45 | );
46 | };
47 |
48 | export default Authors;
49 |
--------------------------------------------------------------------------------
/part5/bloglist-frontend/.eslintrc.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 |
3 | module.exports = {
4 | 'env': {
5 | 'browser': true,
6 | 'es6': true,
7 | 'jest/globals': true,
8 | 'cypress/globals': true
9 | },
10 | 'extends': [
11 | 'eslint:recommended',
12 | 'plugin:react/recommended',
13 | 'plugin:cypress/recommended'
14 | ],
15 | 'parserOptions': {
16 | 'ecmaFeatures': {
17 | 'jsx': true
18 | },
19 | 'ecmaVersion': 2018,
20 | 'sourceType': 'module'
21 | },
22 | 'plugins': [
23 | 'react', 'jest', 'cypress'
24 | ],
25 | 'rules': {
26 | 'indent': [
27 | 'error',
28 | 2
29 | ],
30 | 'linebreak-style': [
31 | 'error',
32 | 'windows'
33 | ],
34 | 'quotes': [
35 | 'error',
36 | 'single'
37 | ],
38 | 'semi': [
39 | 'error',
40 | 'always'
41 | ],
42 | 'eqeqeq': 'error',
43 | 'no-trailing-spaces': 'error',
44 | 'object-curly-spacing': [
45 | 'error', 'always'
46 | ],
47 | 'arrow-spacing': [
48 | 'error', { 'before': true, 'after': true }
49 | ],
50 | 'no-console': 0,
51 | 'react/prop-types': 0
52 | },
53 | 'settings': {
54 | 'react': {
55 | 'version': 'detect'
56 | }
57 | }
58 | };
--------------------------------------------------------------------------------
/part3/phonebook-backend/build/static/js/2.732f3632.chunk.js.LICENSE.txt:
--------------------------------------------------------------------------------
1 | /*
2 | object-assign
3 | (c) Sindre Sorhus
4 | @license MIT
5 | */
6 |
7 | /** @license React v0.20.1
8 | * scheduler.production.min.js
9 | *
10 | * Copyright (c) Facebook, Inc. and its affiliates.
11 | *
12 | * This source code is licensed under the MIT license found in the
13 | * LICENSE file in the root directory of this source tree.
14 | */
15 |
16 | /** @license React v17.0.1
17 | * react-dom.production.min.js
18 | *
19 | * Copyright (c) Facebook, Inc. and its affiliates.
20 | *
21 | * This source code is licensed under the MIT license found in the
22 | * LICENSE file in the root directory of this source tree.
23 | */
24 |
25 | /** @license React v17.0.1
26 | * react-jsx-runtime.production.min.js
27 | *
28 | * Copyright (c) Facebook, Inc. and its affiliates.
29 | *
30 | * This source code is licensed under the MIT license found in the
31 | * LICENSE file in the root directory of this source tree.
32 | */
33 |
34 | /** @license React v17.0.1
35 | * react.production.min.js
36 | *
37 | * Copyright (c) Facebook, Inc. and its affiliates.
38 | *
39 | * This source code is licensed under the MIT license found in the
40 | * LICENSE file in the root directory of this source tree.
41 | */
42 |
--------------------------------------------------------------------------------
/part7/blog-list-frontend-extended/src/reducers/userReducer.js:
--------------------------------------------------------------------------------
1 | import userService from '../services/users';
2 | import handleError from '../utils/handleError';
3 |
4 | const initialState = {
5 | users: [],
6 | status: 'idle'
7 | };
8 |
9 | const reducer = (state = initialState, action) => {
10 | switch (action.type) {
11 | case 'INIT_USERS_PENDING': {
12 | return {
13 | ...state,
14 | status: 'loading',
15 | };
16 | }
17 | case 'INIT_USERS_SUCCESS': {
18 | return {
19 | users: action.payload,
20 | status: 'succeeded',
21 | };
22 | }
23 | case 'INIT_USERS_ERROR': {
24 | return {
25 | ...state,
26 | status: 'failed',
27 | };
28 | }
29 | default:
30 | return state;
31 | }
32 | };
33 |
34 | export const initializeUsers = () => {
35 | return async (dispatch) => {
36 | dispatch({ type: 'INIT_USERS_PENDING' });
37 |
38 | try {
39 | const users = await userService.getAll();
40 |
41 | dispatch({
42 | type: 'INIT_USERS_SUCCESS',
43 | payload: users,
44 | });
45 | } catch (error) {
46 | handleError(error);
47 |
48 | dispatch({ type: 'INIT_USERS_ERROR' });
49 | }
50 | };
51 | };
52 |
53 | export default reducer;
--------------------------------------------------------------------------------
/part8/library-backend/typeDefs.js:
--------------------------------------------------------------------------------
1 | const { gql } = require('apollo-server');
2 |
3 | const typeDefs = gql`
4 | type Author {
5 | name: String!
6 | born: Int
7 | bookCount: Int!
8 | id: ID!
9 | }
10 | type Book {
11 | title: String!
12 | author: Author!
13 | published: Int!
14 | genres: [String!]!
15 | id: ID!
16 | }
17 | type User {
18 | username: String!
19 | favoriteGenre: String!
20 | id: ID!
21 | }
22 | type Token {
23 | value: String!
24 | }
25 | type Query {
26 | bookCount: Int!
27 | authorCount: Int!
28 | allBooks(
29 | author: String
30 | genre: String
31 | ): [Book!]!
32 | allAuthors: [Author!]!
33 | me: User
34 | }
35 | type Mutation {
36 | addBook(
37 | title: String!
38 | author: String!
39 | published: Int!
40 | genres: [String!]!
41 | ): Book!
42 | editAuthor(
43 | name: String!
44 | setBornTo: Int!
45 | ): Author
46 | createUser(
47 | username: String!
48 | favoriteGenre: String!
49 | ): User
50 | login(
51 | username: String!
52 | password: String!
53 | ): Token
54 | }
55 | type Subscription {
56 | bookAdded: Book!
57 | }
58 | `;
59 |
60 | module.exports = { typeDefs };
--------------------------------------------------------------------------------
/part7/blog-list-frontend-extended/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 getAll = async () => {
11 | const response = await axios.get(baseUrl);
12 | return response.data;
13 | };
14 |
15 | const create = async (newObject) => {
16 | const config = {
17 | headers: { Authorization: token },
18 | };
19 |
20 | const response = await axios.post(baseUrl, newObject, config);
21 | return response.data;
22 | };
23 |
24 | const update = async (id, newObject) => {
25 | const config = {
26 | headers: { Authorization: token },
27 | };
28 |
29 | const response = await axios.put(`${baseUrl}/${id}`, newObject, config);
30 | return response.data;
31 | };
32 |
33 | const remove = async (id) => {
34 | const config = {
35 | headers: { Authorization: token },
36 | };
37 |
38 | const response = await axios.delete(`${baseUrl}/${id}`, config);
39 | return response.data;
40 | };
41 |
42 | const comment = async (id, comment) => {
43 | const response = await axios.post(`${baseUrl}/${id}/comments`, { comment });
44 | return response.data;
45 | };
46 |
47 | export default { getAll, create, update, remove, comment, setToken };
--------------------------------------------------------------------------------
/part7/blog-list-frontend-extended/.eslintrc.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 |
3 | module.exports = {
4 | 'env': {
5 | 'browser': true,
6 | 'es6': true,
7 | 'jest/globals': true,
8 | 'cypress/globals': true
9 | },
10 | 'extends': [
11 | 'eslint:recommended',
12 | 'plugin:react/recommended',
13 | 'plugin:cypress/recommended'
14 | ],
15 | 'parserOptions': {
16 | 'ecmaFeatures': {
17 | 'jsx': true
18 | },
19 | 'ecmaVersion': 2018,
20 | 'sourceType': 'module'
21 | },
22 | 'plugins': [
23 | 'react', 'jest', 'cypress'
24 | ],
25 | 'rules': {
26 | 'indent': [
27 | 'error',
28 | 2,
29 | { 'SwitchCase': 1 }
30 | ],
31 | 'linebreak-style': [
32 | 'error',
33 | 'windows'
34 | ],
35 | 'quotes': [
36 | 'error',
37 | 'single'
38 | ],
39 | 'semi': [
40 | 'error',
41 | 'always'
42 | ],
43 | 'eqeqeq': 'error',
44 | 'no-trailing-spaces': 'error',
45 | 'object-curly-spacing': [
46 | 'error', 'always'
47 | ],
48 | 'arrow-spacing': [
49 | 'error', { 'before': true, 'after': true }
50 | ],
51 | 'no-console': 0,
52 | 'react/prop-types': 0
53 | },
54 | 'settings': {
55 | 'react': {
56 | 'version': 'detect'
57 | }
58 | }
59 | };
--------------------------------------------------------------------------------
/part7/blog-list-frontend-extended/src/reducers/notificationReducer.js:
--------------------------------------------------------------------------------
1 | const initialState = {
2 | message: null,
3 | type: null
4 | };
5 |
6 | const reducer = (state = initialState, action) => {
7 | switch (action.type) {
8 | case 'SHOW_NOTIFICATION': {
9 | const { message, type } = action.payload;
10 | return {
11 | message,
12 | type
13 | };
14 | }
15 | case 'HIDE_NOTIFICATION':
16 | return {
17 | message: null,
18 | type: null
19 | };
20 | default:
21 | return state;
22 | }
23 | };
24 |
25 | let timeoutID = null;
26 |
27 | export const showNotificationWithTimeout = (message, type, seconds = 3) => {
28 | return async (dispatch) => {
29 | dispatch(showNotification(message, type));
30 |
31 | if (timeoutID) {
32 | clearTimeout(timeoutID);
33 | }
34 |
35 | timeoutID = setTimeout(() => {
36 | dispatch(hideNotification());
37 | }, seconds * 1000);
38 | };
39 | };
40 |
41 | export const showNotification = (message, type) => {
42 | return {
43 | type: 'SHOW_NOTIFICATION',
44 | payload: {
45 | message,
46 | type
47 | }
48 | };
49 | };
50 |
51 | export const hideNotification = () => {
52 | return { type: 'HIDE_NOTIFICATION' };
53 | };
54 |
55 | export default reducer;
--------------------------------------------------------------------------------
/part9/patientor-backend/src/services/patientService.ts:
--------------------------------------------------------------------------------
1 | import patients from '../../data/patients';
2 | import {
3 | PublicPatient,
4 | NewPatient,
5 | Patient,
6 | NewEntry,
7 | Entry,
8 | } from "../types";
9 | import { v1 as uuid } from 'uuid';
10 |
11 | const getPatients = (): Array => {
12 | return patients.map(({ id, name, dateOfBirth, gender, occupation }) => {
13 | return {
14 | id,
15 | name,
16 | dateOfBirth,
17 | gender,
18 | occupation,
19 | };
20 | });
21 | };
22 |
23 | const addPatient = (newPatient: NewPatient): Patient => {
24 | const patient: Patient = {
25 | id: uuid(),
26 | ...newPatient
27 | };
28 |
29 | patients.push(patient);
30 |
31 | return patient;
32 | };
33 |
34 | const findPatientById = (id: string): Patient | undefined => {
35 | return patients.find((patient) => patient.id === id);
36 | };
37 |
38 | const addEntry = (patientId: string, newEntry: NewEntry): Patient | undefined => {
39 | const entry: Entry = {
40 | id: uuid(),
41 | ...newEntry
42 | };
43 |
44 | const patient = findPatientById(patientId);
45 |
46 | patient?.entries.push(entry);
47 |
48 | return patient;
49 | };
50 |
51 | export default {
52 | getPatients,
53 | addPatient,
54 | findPatientById,
55 | addEntry,
56 | };
--------------------------------------------------------------------------------
/part7/blog-list-frontend-extended/src/components/Togglable.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import Button from '@material-ui/core/Button';
3 | import PropTypes from 'prop-types';
4 |
5 | const Togglable = ({ buttonLabel, children }) => {
6 | const [visible, setVisible] = useState(false);
7 |
8 | const hideWhenVisible = { display: visible ? 'none' : '' };
9 | const showWhenVisible = { display: visible ? '' : 'none' };
10 |
11 | const toggleVisibility = () => {
12 | setVisible(!visible);
13 | };
14 |
15 | const childrenWithVisibilityToggle = React.Children.map(children, child => {
16 | return React.cloneElement(child, { toggleVisibility });
17 | });
18 |
19 | return (
20 |
21 |
22 |
23 | {buttonLabel}
24 |
25 |
26 |
27 | {childrenWithVisibilityToggle}
28 |
29 | Close
30 |
31 |
32 |
33 | );
34 | };
35 |
36 | Togglable.propTypes = {
37 | buttonLabel: PropTypes.string.isRequired,
38 | children: PropTypes.element.isRequired,
39 | };
40 |
41 |
42 | export default Togglable;
--------------------------------------------------------------------------------
/part6/redux-anecdotes/src/components/AnecdoteForm.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { connect } from 'react-redux';
3 | import { addAnecdote } from '../reducers/anecdoteReducer';
4 | import { showNotificationWithTimeout } from '../reducers/notificationReducer';
5 |
6 | const AnecdoteForm = ({ addAnecdote, showNotificationWithTimeout }) => {
7 | const [formValue, setFormValue] = useState('');
8 |
9 | const createAnecdote = async (event) => {
10 | event.preventDefault();
11 |
12 | const anecdote = { content: formValue, votes: 0 };
13 |
14 | addAnecdote(anecdote);
15 |
16 | const message = `Added: '${formValue}'`;
17 | showNotificationWithTimeout(message, 5);
18 |
19 | setFormValue('');
20 | };
21 |
22 | const handleChange = (event) => {
23 | setFormValue(event.target.value);
24 | };
25 |
26 | return (
27 |
38 | );
39 | };
40 |
41 | const mapDispatchToProps = {
42 | addAnecdote,
43 | showNotificationWithTimeout,
44 | };
45 |
46 | export default connect(null, mapDispatchToProps)(AnecdoteForm);
--------------------------------------------------------------------------------
/part1/courseinfo/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 |
4 | const App = () => {
5 | const course = {
6 | name: 'Half Stack application development',
7 | parts: [
8 | {
9 | name: 'Fundamentals of React',
10 | exercises: 10
11 | },
12 | {
13 | name: 'Using props to pass data',
14 | exercises: 7
15 | },
16 | {
17 | name: 'State of a component',
18 | exercises: 14
19 | }
20 | ]
21 | }
22 |
23 | return (
24 |
25 |
26 |
27 |
28 |
29 | )
30 | }
31 |
32 | const Header = ({ course }) => {
33 | return ({course} );
34 | }
35 |
36 | const Content = ({ parts }) => {
37 | return (
38 |
43 | )
44 | }
45 |
46 | const Total = ({ parts }) => {
47 | return (Number of exercises {parts[0].exercises + parts[1].exercises + parts[2].exercises}
);
48 | }
49 |
50 | const Part = ({ part }) => {
51 | return (
52 |
53 | {part.name} {part.exercises}
54 |
55 | )
56 | }
57 |
58 | ReactDOM.render( , document.getElementById('root'))
--------------------------------------------------------------------------------
/part7/blog-list-frontend-extended/src/components/BlogList.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router-dom';
3 | import { useSelector } from 'react-redux';
4 | import AddBlogForm from './AddBlogForm';
5 | import Togglable from './Togglable';
6 | import List from '@material-ui/core/List';
7 | import ListItem from '@material-ui/core/ListItem';
8 |
9 | const BlogList = () => {
10 | const blogs = useSelector((state) => {
11 | return state.blogs.blogs.sort((a, b) => (b.likes || 0) - (a.likes || 0));
12 | });
13 |
14 | const status = useSelector((state) => state.blogs.status);
15 |
16 | const isLoggedIn = useSelector((state) => state.authentication.isLoggedIn);
17 |
18 | if (status === 'failed') return Error: Could not load blogs
;
19 |
20 | if (status === 'loading') return Loading...
;
21 |
22 | return (
23 |
24 |
Blogs
25 | {isLoggedIn &&
26 |
27 | }
28 |
29 | {blogs.map((blog) => {
30 | return (
31 |
32 | {blog.title}
33 |
34 | );
35 | })}
36 |
37 |
38 | );
39 | };
40 |
41 | export default BlogList;
--------------------------------------------------------------------------------
/part9/patientor-backend/src/routes/patients.ts:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import patientService from '../services/patientService';
3 | import { toNewPatient, toNewEntry } from '../utils';
4 |
5 | const router = express.Router();
6 |
7 | router.get('/', (_req, res) => {
8 | res.json(patientService.getPatients());
9 | });
10 |
11 | router.post('/', (req, res) => {
12 | try {
13 | const newPatient = toNewPatient(req.body);
14 |
15 | const addedPatient = patientService.addPatient(newPatient);
16 | res.json(addedPatient);
17 | } catch (error) {
18 | res.status(400).send({ error: error.message });
19 | }
20 | });
21 |
22 | router.get('/:id', (req, res) => {
23 | const patient = patientService.findPatientById(req.params.id);
24 |
25 | if (patient) {
26 | res.json(patient);
27 | } else {
28 | res.sendStatus(404);
29 | }
30 | });
31 |
32 | router.post('/:id/entries', (req, res) => {
33 | try {
34 | const newEntry = toNewEntry(req.body);
35 |
36 | const updatedPatient = patientService.addEntry(req.params.id, newEntry);
37 |
38 | if (!updatedPatient) {
39 | res.status(400).send({ error: "invalid patient id" });
40 | } else {
41 | res.json(updatedPatient);
42 | }
43 | } catch (error) {
44 | res.status(400).send({ error: error.message });
45 | }
46 | });
47 |
48 | export default router;
--------------------------------------------------------------------------------
/part9/patientor-frontend/src/PatientPage/OccupationalHealthcareEntryDetails.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { OccupationalHealthcareEntry } from "../types";
3 | import { Card, Icon } from "semantic-ui-react";
4 | import DiagnosisList from "./DiagnosisList";
5 |
6 | interface OccupationalHealthcareEntryDetailsProps {
7 | entry: OccupationalHealthcareEntry;
8 | }
9 |
10 | const OccupationalHealthcareEntryDetails = ({ entry }: OccupationalHealthcareEntryDetailsProps) => {
11 | return (
12 |
13 |
14 |
15 | {entry.date}
16 |
17 |
18 | {entry.specialist}
19 |
20 | {entry.description}
21 | Employer: {entry.employerName}
22 | {entry.sickLeave &&
23 | Sick Leave: {entry.sickLeave.startDate} to {entry.sickLeave.endDate}
}
24 |
25 |
26 |
27 | {entry.diagnosisCodes
28 | ?
29 | : No diagnosis codes
}
30 |
31 |
32 |
33 | );
34 | };
35 |
36 | export default OccupationalHealthcareEntryDetails;
--------------------------------------------------------------------------------
/part5/bloglist-frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "bloglist-frontend",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/user-event": "^12.6.3",
7 | "axios": "^0.21.1",
8 | "prop-types": "^15.7.2",
9 | "react": "^17.0.1",
10 | "react-dom": "^17.0.1",
11 | "react-scripts": "4.0.1",
12 | "web-vitals": "^0.2.4"
13 | },
14 | "scripts": {
15 | "start": "react-scripts start",
16 | "build": "react-scripts build",
17 | "test": "react-scripts test",
18 | "eject": "react-scripts eject",
19 | "cypress:open": "cypress open",
20 | "test:e2e": "cypress run",
21 | "lint": "eslint ."
22 | },
23 | "eslintConfig": {
24 | "extends": [
25 | "react-app",
26 | "react-app/jest"
27 | ]
28 | },
29 | "browserslist": {
30 | "production": [
31 | ">0.2%",
32 | "not dead",
33 | "not op_mini all"
34 | ],
35 | "development": [
36 | "last 1 chrome version",
37 | "last 1 firefox version",
38 | "last 1 safari version"
39 | ]
40 | },
41 | "proxy": "http://localhost:3003",
42 | "devDependencies": {
43 | "@testing-library/jest-dom": "^5.11.10",
44 | "@testing-library/react": "^11.2.6",
45 | "cypress": "^7.0.1",
46 | "eslint-plugin-cypress": "^2.11.2",
47 | "eslint-plugin-jest": "^24.3.3"
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/part2/courseinfo2/src/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Course from './components/Course';
3 |
4 | const App = () => {
5 | const courses = [
6 | {
7 | name: 'Half Stack application development',
8 | id: 1,
9 | parts: [
10 | {
11 | name: 'Fundamentals of React',
12 | exercises: 10,
13 | id: 1
14 | },
15 | {
16 | name: 'Using props to pass data',
17 | exercises: 7,
18 | id: 2
19 | },
20 | {
21 | name: 'State of a component',
22 | exercises: 14,
23 | id: 3
24 | },
25 | {
26 | name: 'Redux',
27 | exercises: 11,
28 | id: 4
29 | }
30 | ]
31 | },
32 | {
33 | name: 'Node.js',
34 | id: 2,
35 | parts: [
36 | {
37 | name: 'Routing',
38 | exercises: 3,
39 | id: 1
40 | },
41 | {
42 | name: 'Middlewares',
43 | exercises: 7,
44 | id: 2
45 | }
46 | ]
47 | }
48 | ];
49 |
50 | return (
51 |
52 |
Web development curriculum
53 | {courses.map(course =>
54 |
58 | )}
59 |
60 | )
61 | }
62 |
63 | export default App;
--------------------------------------------------------------------------------
/part7/blog-list-frontend-extended/src/components/Navigation.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router-dom';
3 | import AppBar from '@material-ui/core/AppBar';
4 | import ToolBar from '@material-ui/core/ToolBar';
5 | import Button from '@material-ui/core/Button';
6 | import { useDispatch, useSelector } from 'react-redux';
7 | import { logOut } from '../reducers/authenticationReducer';
8 |
9 | const Navigation = () => {
10 |
11 | const dispatch = useDispatch();
12 |
13 | const { user, isLoggedIn } = useSelector((state) => state.authentication);
14 |
15 | const handleLogout = () => {
16 | dispatch(logOut());
17 | };
18 |
19 | return (
20 |
21 |
22 |
23 | Blogs
24 |
25 |
26 | Users
27 |
28 | {isLoggedIn
29 | ? (
30 | Log out
31 | )
32 | : (
33 | Login
34 | )}
35 | {isLoggedIn ? {user.name} is logged in
: null}
36 |
37 |
38 | );
39 | };
40 |
41 | export default Navigation;
--------------------------------------------------------------------------------
/part6/redux-anecdotes/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "redux-anecdotes",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^5.11.9",
7 | "@testing-library/react": "^11.2.5",
8 | "@testing-library/user-event": "^12.6.3",
9 | "axios": "^0.21.1",
10 | "react": "^17.0.1",
11 | "react-dom": "^17.0.1",
12 | "react-redux": "^7.2.2",
13 | "react-scripts": "4.0.2",
14 | "redux": "^4.0.5",
15 | "redux-devtools-extension": "^2.13.9",
16 | "redux-thunk": "^2.3.0",
17 | "web-vitals": "^1.1.0"
18 | },
19 | "scripts": {
20 | "start": "react-scripts start",
21 | "build": "react-scripts build",
22 | "test": "react-scripts test",
23 | "eject": "react-scripts eject",
24 | "lint": "eslint .",
25 | "server": "json-server -p3001 --watch db.json"
26 | },
27 | "proxy": "http://localhost:3001",
28 | "eslintConfig": {
29 | "extends": [
30 | "react-app",
31 | "react-app/jest"
32 | ]
33 | },
34 | "browserslist": {
35 | "production": [
36 | ">0.2%",
37 | "not dead",
38 | "not op_mini all"
39 | ],
40 | "development": [
41 | "last 1 chrome version",
42 | "last 1 firefox version",
43 | "last 1 safari version"
44 | ]
45 | },
46 | "devDependencies": {
47 | "eslint-plugin-jest": "^24.3.5",
48 | "json-server": "^0.16.3"
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/part7/routed-anecdotes/src/components/CreateNew.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useHistory } from 'react-router-dom';
3 | import { useField } from '../hooks';
4 |
5 | const CreateNew = ({ addNew }) => {
6 | const [content, contentReset] = useField('content');
7 | const [author, authorReset] = useField('author');
8 | const [info, infoReset] = useField('info', 'url');
9 |
10 | const history = useHistory();
11 |
12 | const handleSubmit = (event) => {
13 | event.preventDefault();
14 |
15 | addNew({
16 | content: content.value,
17 | author: author.value,
18 | info: info.value,
19 | votes: 0
20 | });
21 |
22 | history.push('/');
23 | };
24 |
25 | const resetFields = () => {
26 | contentReset();
27 | authorReset();
28 | infoReset();
29 | };
30 |
31 | return (
32 |
33 |
create a new anecdote
34 |
50 |
51 | );
52 |
53 | };
54 |
55 | export default CreateNew;
--------------------------------------------------------------------------------
/part4/blog-list-backend/app.js:
--------------------------------------------------------------------------------
1 | const config = require('./utils/config');
2 | const express = require('express');
3 | require('express-async-errors');
4 | const app = express();
5 | const cors = require('cors');
6 | const loginRouter = require('./controllers/login');
7 | const usersRouter = require('./controllers/users');
8 | const blogsRouter = require('./controllers/blogs');
9 | const { requestLogger, tokenExtractor, unknownEndpoint, errorHandler } = require('./utils/middleware');
10 | const logger = require('./utils/logger');
11 | const mongoose = require('mongoose');
12 |
13 | mongoose.connect(
14 | config.MONGODB_URI,
15 | {
16 | useNewUrlParser: true,
17 | useUnifiedTopology: true,
18 | useFindAndModify: false,
19 | useCreateIndex: true
20 | })
21 | .then(() => {
22 | logger.info('connected to MongoDb');
23 | })
24 | .catch((error) => {
25 | logger.error('error connecting to MongoDB:', error.message);
26 | });
27 |
28 | app.use(cors());
29 | app.use(express.json());
30 | app.use(requestLogger);
31 | app.use(tokenExtractor);
32 |
33 | app.use('/api/login', loginRouter);
34 | app.use('/api/users', usersRouter);
35 | app.use('/api/blogs', blogsRouter);
36 |
37 | if (process.env.NODE_ENV === 'test') {
38 | const testingRouter = require('./controllers/testing');
39 | app.use('/api/testing', testingRouter);
40 | }
41 |
42 | app.use(unknownEndpoint);
43 | app.use(errorHandler);
44 |
45 | module.exports = app;
--------------------------------------------------------------------------------
/part7/blog-list-backend-extended/app.js:
--------------------------------------------------------------------------------
1 | const config = require('./utils/config');
2 | const express = require('express');
3 | require('express-async-errors');
4 | const app = express();
5 | const cors = require('cors');
6 | const loginRouter = require('./controllers/login');
7 | const usersRouter = require('./controllers/users');
8 | const blogsRouter = require('./controllers/blogs');
9 | const { requestLogger, tokenExtractor, unknownEndpoint, errorHandler } = require('./utils/middleware');
10 | const logger = require('./utils/logger');
11 | const mongoose = require('mongoose');
12 |
13 | mongoose.connect(
14 | config.MONGODB_URI,
15 | {
16 | useNewUrlParser: true,
17 | useUnifiedTopology: true,
18 | useFindAndModify: false,
19 | useCreateIndex: true
20 | })
21 | .then(() => {
22 | logger.info('connected to MongoDb');
23 | })
24 | .catch((error) => {
25 | logger.error('error connecting to MongoDB:', error.message);
26 | });
27 |
28 | app.use(cors());
29 | app.use(express.json());
30 | app.use(requestLogger);
31 | app.use(tokenExtractor);
32 |
33 | app.use('/api/login', loginRouter);
34 | app.use('/api/users', usersRouter);
35 | app.use('/api/blogs', blogsRouter);
36 |
37 | if (process.env.NODE_ENV === 'test') {
38 | const testingRouter = require('./controllers/testing');
39 | app.use('/api/testing', testingRouter);
40 | }
41 |
42 | app.use(unknownEndpoint);
43 | app.use(errorHandler);
44 |
45 | module.exports = app;
--------------------------------------------------------------------------------
/part5/bloglist-frontend/src/components/AddBlogForm.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render, fireEvent } from '@testing-library/react';
3 | import '@testing-library/jest-dom/extend-expect';
4 | import AddBlogForm from './AddBlogForm';
5 |
6 | describe(' ', () => {
7 | let component;
8 |
9 | const blogObject = {
10 | title: 'Example Blog',
11 | author: 'Jane Doe',
12 | url: 'http://www.example.com/blog',
13 | likes: 0,
14 | };
15 |
16 | const mockAddBlog = jest.fn();
17 |
18 | beforeEach(() => {
19 | component = render(
20 |
21 | );
22 | });
23 |
24 | test('calls the addBlog fn with the correct details during a submit event', () => {
25 | const form = component.container.querySelector('form');
26 | const titleInput = component.getByLabelText(/title/i);
27 | const authorInput = component.getByLabelText(/author/i);
28 | const urlInput = component.getByLabelText(/url/i);
29 |
30 | fireEvent.change(titleInput, {
31 | target: { value: blogObject.title },
32 | });
33 |
34 | fireEvent.change(authorInput, {
35 | target: { value: blogObject.author },
36 | });
37 |
38 | fireEvent.change(urlInput, {
39 | target: { value: blogObject.url },
40 | });
41 |
42 | fireEvent.submit(form);
43 |
44 | expect(mockAddBlog).toHaveBeenCalled();
45 | expect(mockAddBlog).toHaveBeenCalledWith(blogObject);
46 | });
47 | });
--------------------------------------------------------------------------------
/part5/bloglist-frontend/src/components/LoginForm.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | const LoginForm = ({ handleLogin }) => {
5 | const [username, setUsername] = useState('');
6 | const [password, setPassword] = useState('');
7 |
8 | const handleSubmit = (event) => {
9 | event.preventDefault();
10 |
11 | const user = { username, password };
12 | handleLogin(user);
13 |
14 | setUsername('');
15 | setPassword('');
16 | };
17 |
18 | return (
19 |
20 |
Log in to Application
21 |
42 |
43 | );
44 | };
45 |
46 | LoginForm.propTypes = {
47 | handleLogin: PropTypes.func.isRequired,
48 | };
49 |
50 | export default LoginForm;
--------------------------------------------------------------------------------
/part7/blog-list-frontend-extended/src/components/CommentForm.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { useDispatch, useSelector } from 'react-redux';
3 | import { addComment } from '../reducers/blogReducer';
4 | import TextField from '@material-ui/core/TextField';
5 | import Button from '@material-ui/core/Button';
6 | import { makeStyles } from '@material-ui/core/styles';
7 |
8 | const useStyles = makeStyles((theme) => ({
9 | form: {
10 | '& > *': {
11 | marginTop: theme.spacing(1),
12 | marginBottom: theme.spacing(1),
13 | },
14 | },
15 | }));
16 |
17 | const CommentForm = ({ blog }) => {
18 | const classes = useStyles();
19 |
20 | const dispatch = useDispatch();
21 |
22 | const [comment, setComment] = useState('');
23 |
24 | const { isLoggedIn } = useSelector((state) => state.authentication);
25 |
26 | const handleSubmit = async (event) => {
27 | event.preventDefault();
28 |
29 | dispatch(addComment(blog.id, comment));
30 |
31 | setComment('');
32 | };
33 |
34 | if (!isLoggedIn) return null;
35 |
36 | return (
37 |
47 | );
48 | };
49 |
50 | export default CommentForm;
--------------------------------------------------------------------------------
/part8/library-frontend/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 | import { setContext } from '@apollo/client/link/context';
5 | import { getMainDefinition } from '@apollo/client/utilities';
6 | import { WebSocketLink } from '@apollo/client/link/ws';
7 |
8 | import {
9 | ApolloClient,
10 | ApolloProvider,
11 | HttpLink,
12 | InMemoryCache,
13 | split,
14 | } from '@apollo/client';
15 |
16 | const authLink = setContext((_, { headers }) => {
17 | const token = localStorage.getItem('library-user-token');
18 | return {
19 | headers: {
20 | ...headers,
21 | authorization: token ? `Bearer ${token}` : null,
22 | }
23 | };
24 | });
25 |
26 | const httpLink = new HttpLink({ uri: 'http://localhost:4000' });
27 |
28 | const wsLink = new WebSocketLink({
29 | uri: 'ws://localhost:4000/graphql',
30 | options: {
31 | reconnect: true
32 | }
33 | });
34 |
35 | const splitLink = split(
36 | ({ query }) => {
37 | const definition = getMainDefinition(query);
38 | return (
39 | definition.kind === 'OperationDefinition' &&
40 | definition.operation === 'subscription'
41 | );
42 | },
43 | wsLink,
44 | authLink.concat(httpLink),
45 | );
46 |
47 | const client = new ApolloClient({
48 | cache: new InMemoryCache(),
49 | link: splitLink,
50 | });
51 |
52 | ReactDOM.render(
53 |
54 |
55 | ,
56 | document.getElementById('root'));
--------------------------------------------------------------------------------
/part9/patientor-frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "patientor",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "axios": "^0.21.1",
7 | "formik": "^2.2.6",
8 | "react": "^17.0.1",
9 | "react-dom": "^17.0.1",
10 | "react-router-dom": "^5.2.0",
11 | "semantic-ui-css": "^2.4.1",
12 | "semantic-ui-react": "^2.0.3",
13 | "uuid": "^8.3.2",
14 | "yup": "^0.32.9"
15 | },
16 | "devDependencies": {
17 | "@types/axios": "^0.14.0",
18 | "@types/jest": "26.0.20",
19 | "@types/node": "12.11.7",
20 | "@types/react": "^17.0.2",
21 | "@types/react-dom": "^17.0.1",
22 | "@types/react-router-dom": "^5.1.7",
23 | "@types/uuid": "^8.3.0",
24 | "@typescript-eslint/eslint-plugin": "^4.16.1",
25 | "@typescript-eslint/parser": "^4.16.1",
26 | "eslint-config-react": "^1.1.7",
27 | "react-scripts": "^4.0.3",
28 | "typescript": "^4.2.2"
29 | },
30 | "scripts": {
31 | "start": "react-scripts start",
32 | "build": "react-scripts build",
33 | "test": "react-scripts test",
34 | "eject": "react-scripts eject",
35 | "lint": "eslint './src/**/*.{ts,tsx}'",
36 | "lint:fix": "eslint './src/**/*.{ts,tsx}' --fix"
37 | },
38 | "browserslist": {
39 | "production": [
40 | ">0.2%",
41 | "not dead",
42 | "not op_mini all"
43 | ],
44 | "development": [
45 | "last 1 chrome version",
46 | "last 1 firefox version",
47 | "last 1 safari version"
48 | ]
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/part6/redux-anecdotes/src/reducers/anecdoteReducer.js:
--------------------------------------------------------------------------------
1 | import anecdoteService from '../services/anecdotes';
2 |
3 | const reducer = (state = [], action) => {
4 | switch (action.type) {
5 | case 'VOTE':
6 | return state.map((anecdote) => {
7 | return anecdote.id !== action.payload.id
8 | ? anecdote
9 | : { ...anecdote, votes: anecdote.votes + 1 };
10 | });
11 | case 'NEW_ANECDOTE':
12 | return [...state, action.payload];
13 | case 'INIT_ANECDOTES':
14 | return action.payload;
15 | default:
16 | return state;
17 | }
18 | };
19 |
20 | export const voteAnecdote = (id) => {
21 | return async (dispatch, getState) => {
22 | const anecdote = getState().anecdotes.find((anecdote) => anecdote.id === id);
23 |
24 | await anecdoteService.update(id, { ...anecdote, votes: anecdote.votes + 1 });
25 |
26 | dispatch({
27 | type: 'VOTE',
28 | payload: { id }
29 | });
30 | };
31 | };
32 |
33 | export const addAnecdote = (content) => {
34 | return async (dispatch) => {
35 | const anecdote = await anecdoteService.createNew(content);
36 | dispatch({
37 | type: 'NEW_ANECDOTE',
38 | payload: anecdote
39 | });
40 | };
41 | };
42 |
43 | export const initializeAnecdotes = () => {
44 | return async (dispatch) => {
45 | const anecdotes = await anecdoteService.getAll();
46 | dispatch({
47 | type: 'INIT_ANECDOTES',
48 | payload: anecdotes
49 | });
50 | };
51 | };
52 |
53 | export default reducer;
--------------------------------------------------------------------------------
/part9/bmi/bmiCalculator.ts:
--------------------------------------------------------------------------------
1 | export const calculateBmi = (height: number, mass: number): string => {
2 | const bmi = mass / (height / 100) ** 2;
3 |
4 | if (bmi < 15) return 'Very severly underweight';
5 |
6 | if (bmi < 16) return 'Severely underweight';
7 |
8 | if (bmi < 18.5) return 'Underweight';
9 |
10 | if (bmi < 25) return 'Normal (healthy weight)';
11 |
12 | if (bmi < 30) return 'Overweight';
13 |
14 | if (bmi < 35) return 'Obese Class I (Moderately obese)';
15 |
16 | if (bmi < 40) return 'Obese Class II (Severely obese)';
17 |
18 | if (bmi >= 40) return 'Obese Class III (Very severely obese)';
19 |
20 | return 'Error: Unable to calculate BMI';
21 | };
22 |
23 | interface bmiArgs {
24 | height: number;
25 | weight: number;
26 | }
27 |
28 | const parseBmiArguments = (args: Array): bmiArgs => {
29 | if (args.length < 4) throw new Error('Not enough arguments');
30 | if (args.length > 4) throw new Error('Too many arguments');
31 |
32 | if (isNaN(Number(args[2])) && !isNaN(Number(args[3]))) {
33 | throw new Error('Provided values were not numbers!');
34 | }
35 |
36 | return {
37 | height: Number(args[2]),
38 | weight: Number(args[3])
39 | };
40 | };
41 |
42 | if (require.main === module) {
43 | try {
44 | const { height, weight } = parseBmiArguments(process.argv);
45 | console.log(calculateBmi(height, weight));
46 | } catch (error) {
47 | if (error instanceof Error) {
48 | console.log("An error has occured: ", error.message);
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/part8/library-frontend/src/components/Books.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { useQuery } from '@apollo/client';
3 | import { ALL_BOOKS } from '../queries';
4 | import BookTable from './BookTable';
5 |
6 | const Books = () => {
7 | const [filter, setFilter] = useState('all');
8 |
9 | const { data: booksData, loading: booksLoading, error: booksError } = useQuery(ALL_BOOKS);
10 |
11 | const genres = [...(new Set(booksData?.allBooks.flatMap((book) => book.genres)))];
12 | const options = ['all'].concat(genres);
13 |
14 | const { data: filteredBooksData, loading: filteredBooksLoading, error: filteredBooksError } = useQuery(ALL_BOOKS, {
15 | variables: { genre: filter !== 'all' ? filter : null },
16 | fetchPolicy: 'cache-and-network',
17 | });
18 |
19 | const books = filteredBooksData?.allBooks;
20 |
21 | if (booksLoading || filteredBooksLoading) {
22 | return loading...
;
23 | }
24 |
25 | if (booksError || filteredBooksError) {
26 | return Error: Could not load books
;
27 | }
28 |
29 | return (
30 |
31 |
Books
32 |
show:
33 |
setFilter(target.value)}>
34 | {options.map((option) => {
35 | return (
36 |
37 | {option}
38 |
39 | );
40 | })}
41 |
42 |
43 |
44 | );
45 | };
46 |
47 | export default Books;
--------------------------------------------------------------------------------
/part9/courseinfo-typescript/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Header from './components/Header';
3 | import Content from './components/Content';
4 | import Total from './components/Total';
5 | import { CoursePart } from './types';
6 |
7 | const App = () => {
8 | const courseName = "Half Stack application development";
9 | const courseParts: CoursePart[] = [
10 | {
11 | name: "Fundamentals",
12 | exerciseCount: 10,
13 | description: "This is the leisured course part",
14 | type: "normal"
15 | },
16 | {
17 | name: "Advanced",
18 | exerciseCount: 7,
19 | description: "This is the harded course part",
20 | type: "normal"
21 | },
22 | {
23 | name: "Using props to pass data",
24 | exerciseCount: 7,
25 | groupProjectCount: 3,
26 | type: "groupProject"
27 | },
28 | {
29 | name: "Deeper type usage",
30 | exerciseCount: 14,
31 | description: "Confusing description",
32 | exerciseSubmissionLink: "https://fake-exercise-submit.made-up-url.dev",
33 | type: "submission"
34 | },
35 | {
36 | name: "Backend development",
37 | exerciseCount: 21,
38 | description: "Typing the backend",
39 | requirements: ["nodejs", "jest"],
40 | type: "special"
41 | },
42 | ]
43 |
44 | return (
45 |
46 |
50 | );
51 | };
52 |
53 | export default App;
--------------------------------------------------------------------------------
/part3/phonebook-backend/build/static/js/runtime-main.a212f097.js:
--------------------------------------------------------------------------------
1 | !function(e){function r(r){for(var n,l,p=r[0],f=r[1],i=r[2],c=0,s=[];c {
11 | const vote = (id) => {
12 | voteAnecdote(id);
13 |
14 | const anecdote = anecdotes.find((anecdote) => {
15 | return anecdote.id === id;
16 | });
17 |
18 | const message = `Voted for: '${anecdote.content}'`;
19 | showNotificationWithTimeout(message, 5);
20 | };
21 |
22 | return (
23 |
24 | {anecdotes.sort((a, b) => b.votes - a.votes).map(anecdote =>
25 |
26 |
27 | {anecdote.content}
28 |
29 |
30 | has {anecdote.votes}
31 | vote(anecdote.id)}>vote
32 |
33 |
34 | )}
35 |
36 | );
37 | };
38 |
39 | const mapStateToProps = ({ anecdotes, filter }) => {
40 | const filteredAnecdotes = anecdotes.filter((anecdote) => {
41 | return anecdote.content.toLowerCase().includes(filter.toLowerCase());
42 | });
43 |
44 | return {
45 | anecdotes: filteredAnecdotes
46 | };
47 | };
48 |
49 | const mapDispatchToProps = {
50 | voteAnecdote,
51 | showNotificationWithTimeout,
52 | };
53 |
54 | export default connect(mapStateToProps, mapDispatchToProps)(AnecdoteList);
--------------------------------------------------------------------------------
/part8/library-frontend/src/components/BirthYearForm.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { useMutation } from '@apollo/client';
3 | import { EDIT_AUTHOR } from '../queries';
4 |
5 | const BirthYearForm = ({ authors }) => {
6 | const [name, setName] = useState(authors[0].name);
7 | const [born, setBorn] = useState('');
8 |
9 | const [editAuthor] = useMutation(EDIT_AUTHOR, {
10 | onError: (error) => {
11 | console.error(error);
12 | }
13 | });
14 |
15 | const submit = async (event) => {
16 | event.preventDefault();
17 |
18 | const bornInt = Number.parseInt(born, 10);
19 |
20 | editAuthor({
21 | variables: {
22 | name,
23 | born: bornInt,
24 | }
25 | });
26 |
27 | setBorn('');
28 | };
29 |
30 | return (
31 |
32 |
Set birth year
33 |
53 |
54 | );
55 | };
56 |
57 | export default BirthYearForm;
--------------------------------------------------------------------------------
/part9/patientor-frontend/README.md:
--------------------------------------------------------------------------------
1 | # Patientor - frontend
2 |
3 | A medical record app that provides and updates patient details and medical records. The provided front end was expanded to add pages for displaying patient details and medical records. Formik and Yup were used to create a maintainable form for adding medical records. Semantic UI React was also used.
4 |
5 | ## Screenshots
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | ## Available Scripts
14 |
15 | In the project directory, you can run:
16 |
17 | ### `npm install`
18 |
19 | Install the project dependencies.
20 |
21 | ### `npm start`
22 |
23 | Runs the app in the development mode.
24 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
25 |
26 | The page will reload if you make edits.
27 | You will also see any lint errors in the console.
28 |
29 | ### `npm build`
30 |
31 | Builds the app for production to the `build` folder.
32 | It correctly bundles React in production mode and optimizes the build for the best performance.
33 |
34 | The build is minified and the filenames include the hashes.
35 | Your app is ready to be deployed!
36 |
37 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
38 |
--------------------------------------------------------------------------------
/part3/phonebook-backend/mongo.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 |
3 | if (process.argv.length <= 2 || process.argv.length === 4 || process.argv.length >= 6) {
4 | console.log('Please provide the password as an argument: node mongo.js \nOptionally provide a name and number to add a person to the database: node mongo.js ');
5 | process.exit(1);
6 | }
7 |
8 | const password = process.argv[2];
9 | const name = process.argv[3];
10 | const number = process.argv[4];
11 |
12 | const url = `mongodb+srv://ruel:${password}@cluster0.e31yt.mongodb.net/phonebook-app?retryWrites=true&w=majority`;
13 |
14 | mongoose
15 | .connect(url, { useNewUrlParser: true, useUnifiedTopology: true, useFindAndModify: false, useCreateIndex: true })
16 | .catch((error) => {
17 | console.log('Error connecting to MongoDB', error.message);
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 | if (process.argv.length === 3) {
28 | console.log('Phonebook:');
29 | Person.find({}).then(result => {
30 | result.forEach(person => {
31 | console.log(`${person.name} ${person.number}`);
32 | });
33 | mongoose.connection.close();
34 | });
35 | }
36 |
37 | if (process.argv.length === 5) {
38 | const person = new Person({
39 | name: name,
40 | number: number,
41 | });
42 |
43 | person.save().then(result => {
44 | console.log(`Added ${result.name} with number ${result.number} to the phonebook`);
45 | mongoose.connection.close();
46 | });
47 | }
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/part2/countries/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import Search from './components/Search';
3 | import Results from './components/Results';
4 |
5 | import axios from 'axios';
6 |
7 | const App = () => {
8 | const [query, setQuery] = useState('');
9 | const [countries, setCountries] = useState();
10 | const [gettingCountries, setGettingCountries] = useState(true);
11 | const [isError, setIsError] = useState(false);
12 |
13 | const updateCountries = () => {
14 | axios
15 | .get('https://restcountries.eu/rest/v2/all')
16 | .then(response => {
17 | setCountries(response.data)
18 | setGettingCountries(false);
19 | })
20 | .catch(function (error) {
21 | if (error.response) {
22 | console.error(error.response.data);
23 | } else if (error.request) {
24 | console.error(error.request);
25 | } else {
26 | console.error('Error', error.message);
27 | }
28 | setIsError(true);
29 | });
30 | }
31 |
32 | useEffect(updateCountries, []);
33 |
34 | const handleQuery = (event) => {
35 | setQuery(event.target.value);
36 | }
37 |
38 | return (
39 |
40 |
44 | {gettingCountries && !isError &&
Getting countries data...
}
45 | {isError &&
Error: Could not get countries data
}
46 | {!gettingCountries &&
}
51 |
52 | );
53 | }
54 |
55 | export default App;
56 |
--------------------------------------------------------------------------------