├── documentation
├── Layout.png
├── state.png
├── designdocs
│ ├── login.png
│ ├── signup.png
│ ├── bookinfo.png
│ ├── mockupflow.png
│ ├── navbaropen.png
│ ├── landingpage.png
│ ├── userlibrary.png
│ ├── userprofile.png
│ ├── passwordreset.png
│ ├── emailnotification.png
│ ├── figmadesignoverview.png
│ └── design.md
├── theme.md
└── routes.md
├── frontend
├── public
│ ├── robots.txt
│ ├── favicon.ico
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ ├── index.html
│ ├── library_icon.svg
│ └── library_logo.svg
├── src
│ ├── fonts
│ │ ├── Quicksand.woff2
│ │ ├── Raleway.woff2
│ │ ├── Raleway-Italic.woff2
│ │ ├── fontWeight.scss
│ │ └── font-face.scss
│ ├── stylesUtils
│ │ ├── sizes.scss
│ │ ├── noBulletList.scss
│ │ ├── stack.scss
│ │ ├── userText.scss
│ │ └── mixins
│ │ │ └── mediaqueries.scss
│ ├── components
│ │ ├── Footer.jsx
│ │ ├── BookCard.jsx
│ │ ├── Navigation.jsx
│ │ ├── Header.jsx
│ │ ├── navigation.scss
│ │ └── header.scss
│ ├── SERVER.js
│ ├── pages
│ │ ├── About.jsx
│ │ ├── Profile.jsx
│ │ ├── Login.jsx
│ │ ├── Layout.jsx
│ │ ├── Book.jsx
│ │ ├── login.scss
│ │ ├── Home.jsx
│ │ └── home.scss
│ ├── setupTests.js
│ ├── App.jsx
│ ├── App.test.js
│ ├── index.css
│ ├── index.js
│ ├── App.scss
│ ├── serviceWorker.js
│ └── library_logo.svg
├── index.html
├── client.js
├── package.json
└── README.md
├── .gitignore
├── .prettierrc.json
├── apidoc.json
├── learning
└── datastructures
│ ├── ReadMe.txt
│ ├── index.html
│ ├── data.js
│ ├── test.js
│ ├── datastructures.js
│ └── expected.js
├── db
├── schemas.js
└── mongoose.js
├── mongo-replicaset
└── docker-compose.yml
├── server.js
├── package.json
├── test
├── books.test.js
└── users.test.js
├── LICENSE
├── routers
├── users.js
└── books.js
└── README.md
/documentation/Layout.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/threedevs/3dev-fullstack/HEAD/documentation/Layout.png
--------------------------------------------------------------------------------
/documentation/state.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/threedevs/3dev-fullstack/HEAD/documentation/state.png
--------------------------------------------------------------------------------
/frontend/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .env
2 | .vscode
3 | node_modules
4 | frontend/node_modules
5 | .DS_Store
6 | out
7 | mongo-replicaset/data
--------------------------------------------------------------------------------
/frontend/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/threedevs/3dev-fullstack/HEAD/frontend/public/favicon.ico
--------------------------------------------------------------------------------
/frontend/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/threedevs/3dev-fullstack/HEAD/frontend/public/logo192.png
--------------------------------------------------------------------------------
/frontend/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/threedevs/3dev-fullstack/HEAD/frontend/public/logo512.png
--------------------------------------------------------------------------------
/documentation/designdocs/login.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/threedevs/3dev-fullstack/HEAD/documentation/designdocs/login.png
--------------------------------------------------------------------------------
/frontend/src/fonts/Quicksand.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/threedevs/3dev-fullstack/HEAD/frontend/src/fonts/Quicksand.woff2
--------------------------------------------------------------------------------
/frontend/src/fonts/Raleway.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/threedevs/3dev-fullstack/HEAD/frontend/src/fonts/Raleway.woff2
--------------------------------------------------------------------------------
/documentation/designdocs/signup.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/threedevs/3dev-fullstack/HEAD/documentation/designdocs/signup.png
--------------------------------------------------------------------------------
/documentation/designdocs/bookinfo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/threedevs/3dev-fullstack/HEAD/documentation/designdocs/bookinfo.png
--------------------------------------------------------------------------------
/documentation/designdocs/mockupflow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/threedevs/3dev-fullstack/HEAD/documentation/designdocs/mockupflow.png
--------------------------------------------------------------------------------
/documentation/designdocs/navbaropen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/threedevs/3dev-fullstack/HEAD/documentation/designdocs/navbaropen.png
--------------------------------------------------------------------------------
/frontend/src/fonts/Raleway-Italic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/threedevs/3dev-fullstack/HEAD/frontend/src/fonts/Raleway-Italic.woff2
--------------------------------------------------------------------------------
/documentation/designdocs/landingpage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/threedevs/3dev-fullstack/HEAD/documentation/designdocs/landingpage.png
--------------------------------------------------------------------------------
/documentation/designdocs/userlibrary.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/threedevs/3dev-fullstack/HEAD/documentation/designdocs/userlibrary.png
--------------------------------------------------------------------------------
/documentation/designdocs/userprofile.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/threedevs/3dev-fullstack/HEAD/documentation/designdocs/userprofile.png
--------------------------------------------------------------------------------
/frontend/src/stylesUtils/sizes.scss:
--------------------------------------------------------------------------------
1 | $sm: 31;
2 | $md: 48;
3 | $xl: 67;
4 | $xxl: 110;
5 | $media1920: 120;
6 |
7 | $navigationSwitch: 85;
8 |
--------------------------------------------------------------------------------
/documentation/designdocs/passwordreset.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/threedevs/3dev-fullstack/HEAD/documentation/designdocs/passwordreset.png
--------------------------------------------------------------------------------
/documentation/designdocs/emailnotification.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/threedevs/3dev-fullstack/HEAD/documentation/designdocs/emailnotification.png
--------------------------------------------------------------------------------
/documentation/designdocs/figmadesignoverview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/threedevs/3dev-fullstack/HEAD/documentation/designdocs/figmadesignoverview.png
--------------------------------------------------------------------------------
/frontend/src/components/Footer.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default function Footer() {
4 | // TODO add content
5 |
6 | return
;
7 | }
8 |
--------------------------------------------------------------------------------
/frontend/src/SERVER.js:
--------------------------------------------------------------------------------
1 | const databaseURL = 'http://localhost:1337';
2 |
3 | export const allBooksURL = `${databaseURL}/api/books`;
4 | export const bookURL = `${databaseURL}/api/books/`;
5 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "es5",
3 | "tabWidth": 4,
4 | "singleQuote": true,
5 | "printWidth": 150,
6 | "jsxBracketSameLine": true,
7 | "useTabs": true
8 | }
--------------------------------------------------------------------------------
/frontend/src/stylesUtils/noBulletList.scss:
--------------------------------------------------------------------------------
1 | %NoBulletList {
2 | list-style-type: none;
3 |
4 | > li::before {
5 | display: block;
6 | height: 0;
7 |
8 | content: '\200B';
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/frontend/src/fonts/fontWeight.scss:
--------------------------------------------------------------------------------
1 | $thin: 100;
2 | $extra-light: 200;
3 | $light: 300;
4 | $regular: 400;
5 | $medium: 500;
6 | $semi-bold: 600;
7 | $bold: 700;
8 | $extra-bold: 800;
9 | $black: 900;
10 |
--------------------------------------------------------------------------------
/apidoc.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "3dev-fullstack",
3 | "version": "0.1.0",
4 | "description": "A practice full stack application for 3devs",
5 | "title": "Custom apiDoc browser title",
6 | "url": "https://localhost:1337"
7 | }
8 |
--------------------------------------------------------------------------------
/frontend/src/pages/About.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default function About() {
4 | return (
5 | <>
6 | About page
7 | this is the about page
8 | >
9 | );
10 | }
11 |
--------------------------------------------------------------------------------
/learning/datastructures/ReadMe.txt:
--------------------------------------------------------------------------------
1 | - you can just usa a liveserver with index.html
2 | - solve the exercises in datastructure.js
3 | - data.js is holding the data you need for the exercises
4 | - the tests are run on reload or clicking the button
--------------------------------------------------------------------------------
/frontend/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom/extend-expect';
6 |
--------------------------------------------------------------------------------
/frontend/src/App.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import './App.scss';
3 | import Layout from './pages/Layout';
4 | import { BrowserRouter as Router } from 'react-router-dom';
5 |
6 | function App() {
7 | return (
8 |
9 |
10 |
11 | );
12 | }
13 |
14 | export default App;
15 |
--------------------------------------------------------------------------------
/frontend/src/pages/Profile.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useParams } from 'react-router-dom';
3 |
4 | export default function Profile() {
5 | let { id } = useParams();
6 |
7 | return (
8 | <>
9 | Profile
10 | Change your profile
11 | >
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/frontend/src/App.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from '@testing-library/react';
3 | import App from './App';
4 |
5 | test('renders learn react link', () => {
6 | const { getByText } = render();
7 | const linkElement = getByText(/learn react/i);
8 | expect(linkElement).toBeInTheDocument();
9 | });
10 |
--------------------------------------------------------------------------------
/frontend/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
4 | sans-serif;
5 | -webkit-font-smoothing: antialiased;
6 | -moz-osx-font-smoothing: grayscale;
7 | }
8 |
9 | code {
10 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
11 | }
12 |
--------------------------------------------------------------------------------
/frontend/src/stylesUtils/stack.scss:
--------------------------------------------------------------------------------
1 | %Stack {
2 | --stackMargin: 1.2em;
3 |
4 | > * {
5 | margin-top: 0;
6 | margin-bottom: 0;
7 |
8 | + * {
9 | margin-top: var(--stackMargin);
10 | }
11 | }
12 |
13 | &--recursive {
14 | --stackMargin: 1.2em;
15 |
16 | * {
17 | margin-top: 0;
18 | margin-bottom: 0;
19 |
20 | + * {
21 | margin-top: var(--stackMargin);
22 | }
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/db/schemas.js:
--------------------------------------------------------------------------------
1 | const user = {
2 | username: { type: String, required: true, unique: true },
3 | password: { type: String, required: true },
4 | };
5 |
6 | const book = {
7 | title: { type: String },
8 | author: { type: String },
9 | genre: { type: String },
10 | yearPublished: { type: String },
11 | dateAdded: { type: Date, default: Date.now() },
12 | };
13 |
14 | module.exports = {
15 | book,
16 | user,
17 | };
18 |
--------------------------------------------------------------------------------
/mongo-replicaset/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.3'
2 | services:
3 | docker-mongo-local-replicaset:
4 | ports:
5 | - '27001:27001'
6 | - '27002:27002'
7 | - '27003:27003'
8 | container_name: mongo
9 | volumes:
10 | - ./data/mongodb:/data
11 | environment:
12 | - REPLICA_SET_NAME=mongo-rs
13 | restart: always
14 | image: flqw/docker-mongo-local-replicaset
15 |
--------------------------------------------------------------------------------
/frontend/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Snippets
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/documentation/theme.md:
--------------------------------------------------------------------------------
1 | colors
2 |
3 | 5BA788 primary
4 | 323031 navigation background
5 | 3D3B3C content background
6 | B75252 cancel buttons
7 | FFFFFF fonts
8 |
9 |
10 | fonts
11 |
12 | "Quicksand" for titles, "Raleway" for paragraphs/texts
13 | https://fonts.google.com/specimen/Quicksand?vfonly=true#glyphs
14 | https://fonts.google.com/specimen/Raleway?vfonly=true#glyphs
15 |
16 |
17 | logos:
18 |
19 | main logo:
20 | /frontend/public/library log.svg
21 |
22 | favico:
23 | /frontend/public/library icon.svg
--------------------------------------------------------------------------------
/frontend/src/stylesUtils/userText.scss:
--------------------------------------------------------------------------------
1 | @import 'stack';
2 | @import '../fonts/fontWeight';
3 |
4 | %UserText {
5 | display: flex;
6 | flex-direction: column;
7 |
8 | @extend %Stack;
9 |
10 | h3 {
11 | font: $bold 1.75em/1.2 'Raleway', sans-serif;
12 | }
13 |
14 | h4 {
15 | font: $bold 1.3em/1.2 'Raleway', sans-serif;
16 | }
17 |
18 | p,
19 | li {
20 | font: $regular 1em/1.75 'Raleway', sans-serif;
21 | }
22 |
23 | a {
24 | text-decoration: none;
25 | color: currentColor;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/frontend/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 | import * as serviceWorker from './serviceWorker';
6 |
7 | ReactDOM.render(
8 |
9 |
10 | ,
11 | document.getElementById('root')
12 | );
13 |
14 | // If you want your app to work offline and load faster, you can change
15 | // unregister() to register() below. Note this comes with some pitfalls.
16 | // Learn more about service workers: https://bit.ly/CRA-PWA
17 | serviceWorker.unregister();
18 |
--------------------------------------------------------------------------------
/frontend/client.js:
--------------------------------------------------------------------------------
1 | window.onload = () => {
2 | //make a POST request
3 | fetch('http://localhost:3000', {
4 | method: 'POST',
5 | headers: { 'Content-Type': 'application/json' },
6 | body: JSON.stringify({ username: 'testuser2', password: 'supersafepw', password2: 'supersafepw' }),
7 | })
8 | .then((res) => {
9 | console.log(res);
10 | return res.status === 200 ? res.json() : Promise.reject();
11 | })
12 | .then((data) => {
13 | console.log(data);
14 | })
15 | .catch((err) => {
16 | console.error(err);
17 | })
18 | .finally(() => {
19 | console.log('done');
20 | });
21 | };
22 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/frontend/src/fonts/font-face.scss:
--------------------------------------------------------------------------------
1 | @import 'fontWeight';
2 |
3 | @font-face {
4 | font-family: 'Quicksand';
5 | src: url('Quicksand.woff2') format('woff2');
6 | font-style: normal;
7 | font-weight: $thin $black;
8 | font-display: swap;
9 | }
10 |
11 | @font-face {
12 | font-family: 'Raleway';
13 | font-style: normal;
14 | font-weight: $thin $black;
15 | src: url('Raleway.woff2') format('woff2');
16 | font-display: swap;
17 | }
18 |
19 | @font-face {
20 | font-family: 'Raleway Italic';
21 | font-style: italic;
22 | font-weight: $thin $black;
23 | src: url('Raleway-Italic.woff2') format('woff2');
24 | font-display: swap;
25 | }
26 |
--------------------------------------------------------------------------------
/learning/datastructures/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Snippets
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/frontend/src/components/BookCard.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router-dom';
3 | export default function BookCard({ book }) {
4 | return (
5 |
6 | {book.title}
7 |
8 | - author :
9 | - {book.author}
10 | - Published in :
11 | - {book.yearPublished}
12 |
13 |
14 |
15 |
16 | See more
17 |
18 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/frontend/src/pages/Login.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import './login.scss';
3 |
4 | export default function Login() {
5 | return (
6 |
16 | );
17 | }
--------------------------------------------------------------------------------
/frontend/src/stylesUtils/mixins/mediaqueries.scss:
--------------------------------------------------------------------------------
1 | @import '../sizes';
2 |
3 | @mixin mediaSm {
4 | @media screen and (min-width: #{$sm}em) {
5 | @content;
6 | }
7 | }
8 |
9 | @mixin mediaMd {
10 | @media screen and (min-width: #{$md}em) {
11 | @content;
12 | }
13 | }
14 |
15 | @mixin mediaXl {
16 | @media screen and (min-width: #{$xl}em) {
17 | @content;
18 | }
19 | }
20 |
21 | @mixin mediaXxl {
22 | @media screen and (min-width: #{$xxl}em) {
23 | @content;
24 | }
25 | }
26 |
27 | @mixin media1920 {
28 | @media screen and (min-width: #{$media1920}em) {
29 | @content;
30 | }
31 | }
32 |
33 | @mixin navigationSwitch {
34 | @media screen and (min-width: #{$navigationSwitch}em) {
35 | @content;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/documentation/routes.md:
--------------------------------------------------------------------------------
1 | - get all books, GET, /api/books
2 | - get one book by id, GET, /api/books/:id
3 | - search multiple by title, GET, /api/books/s/:search
4 | - create a new book, POST, /api/books
5 | - delete a book, DELETE, /api/books/:id
6 | - updates a book, PATCH, /api/books/:id
7 | - get all books of same genre, GET, /api/books/:genre
8 |
9 | - get user by id, GET, /api/users/:id
10 | - create a new user, POST, /api/auth/register
11 | - user login, POST, /api/auth/login
12 | - deletes a user, DELETE, /api/profile/:id
13 | - updates a user, PATCH, /api/profile/:id
14 |
--------------------------------------------------------------------------------
/frontend/src/pages/Layout.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Header from '../components/Header';
3 | import Footer from '../components/Footer';
4 | import Home from './Home';
5 | import { Switch, Route } from 'react-router-dom';
6 | import About from './About';
7 | import Login from './Login';
8 | import Book from './Book';
9 | import Profile from './Profile';
10 |
11 | export default function Layout() {
12 | return (
13 | <>
14 |
15 |
16 |
17 | } />
18 | } />
19 | } />
20 | } />
21 | } />
22 |
23 |
24 |
25 | >
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/frontend/src/components/Navigation.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import './navigation.scss';
3 | import { Link } from 'react-router-dom';
4 |
5 | function NavItem({ route, name }) {
6 | return (
7 |
8 |
9 | {name}
10 |
11 |
12 | );
13 | }
14 |
15 | export default function Navigation(props) {
16 | return (
17 |
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/documentation/designdocs/design.md:
--------------------------------------------------------------------------------
1 | 28/10
2 |
3 | added screenshots of the mock up and overall layout of design works from figma
4 |
5 | Project can be viewed below
6 | https://www.figma.com/file/XIUfEV7DlJHJCxwN8BAz5U/Library?node-id=0%3A1
7 |
8 |
9 | Prototype is up (still in progress) will confirm if its viewable seperately and share link(complete)
10 |
11 |
12 | 29/10
13 |
14 | added lost password, and email confirmation pages
15 |
16 | modified signup page to include email adress field
17 |
18 | modified log in page to include a link to sign in page for non account holders
19 |
20 |
21 | updated prototype to imclude all click activity as i can anticipate
22 | i am looking into the carousel behaviour for the landing page main area
23 |
24 | prototype can be viewed here:
25 |
26 | https://www.figma.com/proto/XIUfEV7DlJHJCxwN8BAz5U/Library?node-id=45%3A233&viewport=727%2C-396%2C0.13154926896095276&scaling=scale-down
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "library",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^4.2.4",
7 | "@testing-library/react": "^9.3.2",
8 | "@testing-library/user-event": "^7.1.2",
9 | "node-sass": "^4.14.1",
10 | "normalize.css": "^8.0.1",
11 | "react": "^16.14.0",
12 | "react-dom": "^16.14.0",
13 | "react-router-dom": "^5.2.0",
14 | "react-scripts": "3.4.3"
15 | },
16 | "scripts": {
17 | "start": "react-scripts start",
18 | "build": "react-scripts build",
19 | "test": "react-scripts test",
20 | "eject": "react-scripts eject"
21 | },
22 | "eslintConfig": {
23 | "extends": "react-app"
24 | },
25 | "browserslist": {
26 | "production": [
27 | ">0.2%",
28 | "not dead",
29 | "not op_mini all"
30 | ],
31 | "development": [
32 | "last 1 chrome version",
33 | "last 1 firefox version",
34 | "last 1 safari version"
35 | ]
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/frontend/src/pages/Book.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import { useParams } from 'react-router-dom';
3 |
4 | import { bookURL } from '../SERVER';
5 |
6 | export default function Book() {
7 | const { id } = useParams();
8 | const [book, setBook] = useState({});
9 |
10 | useEffect(() => {
11 | fetch(`${bookURL}${id}`)
12 | .then((response) => {
13 | if (response.status === 200) {
14 | return response.json();
15 | } else {
16 | return Promise.reject();
17 | }
18 | })
19 | .then((data) => setBook(data))
20 | .catch((err) => console.error(err));
21 | }, []);
22 |
23 | return (
24 | <>
25 | {book.title}
26 |
27 | - author
28 | - {book.author}
29 | - genre
30 | - {book.genre}
31 | - year
32 | - {book.yearPublished}
33 |
34 |
35 | >
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/frontend/src/components/Header.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import Navigation from './Navigation';
3 | import { ReactComponent as Logo } from '../library_logo.svg';
4 | import './header.scss';
5 | import { Link } from 'react-router-dom';
6 |
7 | export default function Header() {
8 | const [navOpen, setNavOpen] = useState(false);
9 |
10 | return (
11 |
12 |
13 |
14 |
15 | Library
16 |
17 |
18 |
19 | {/* TODO add search form */}
20 |
26 |
27 |
28 | );
29 | }
30 |
--------------------------------------------------------------------------------
/frontend/src/pages/login.scss:
--------------------------------------------------------------------------------
1 | @import '../fonts/fontWeight.scss';
2 |
3 | .PageTitle {
4 | margin: 0 0 1.25em ;
5 | font: $regular 2.25em/1.5 'Quicksand', sans-serif;
6 | text-transform: uppercase;
7 | }
8 |
9 | .Section {
10 | padding: 2em;
11 | text-align: center;
12 | }
13 |
14 | .Input {
15 | background-color: #5BA788;
16 | border: none;
17 | color: white;
18 | width: 20em;
19 | height: 2em;
20 | border-radius: 2em;
21 | }
22 |
23 | .Label {
24 | margin: 0.5em;
25 | //padding-left: 0.5em;
26 | padding: 0 0 10em 0em;
27 | font-style: 'Quicksand', sans-serif;
28 | font-size: larger;
29 | text-transform: uppercase;
30 | }
31 |
32 | .PasswordLink {
33 | color: white;
34 | font-style: 'Quicksand', sans-serif;
35 | }
36 | .Button {
37 | border: none;
38 | color: white;
39 | padding: 0.5em ;
40 | text-align: center;
41 | font-size: 1.5em;
42 | margin: 0 2em;
43 | display: inline-block;
44 | text-transform: uppercase;
45 | cursor: pointer;
46 | border-radius: 0.2em;
47 | background-color: #5BA788;
48 | }
--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const app = express();
3 | const port = 1337;
4 | const bodyParser = require('body-parser');
5 | const userRouter = require('./routers/users');
6 | const bookRouter = require('./routers/books');
7 |
8 | require('dotenv').config();
9 |
10 | //cors settings, adapt to your needs in production
11 | app.all('*', (req, res, next) => {
12 | res.header('Access-Control-Allow-Origin', '*');
13 | res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization, content-type');
14 | res.header('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS, PATCH');
15 | res.header('Access-Control-Expose-Headers', 'Authorization');
16 | res.header('Access-Control-Allow-Credentials', true);
17 | next();
18 | });
19 |
20 | app.use(bodyParser.json({ limit: '50mb' }));
21 | app.use(bodyParser.urlencoded({ extended: true }));
22 |
23 | app.use('/api/users', userRouter);
24 | app.use('/api/books', bookRouter);
25 |
26 | app.listen(port, () => {
27 | console.log(`Example app listening at http://localhost:${port}`);
28 | });
29 |
30 | module.exports = app
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "learning",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "server.js",
6 | "engines": {
7 | "node": "12.7.0"
8 | },
9 | "scripts": {
10 | "test": "mocha test",
11 | "start": "node server.js",
12 | "frontend": "cd frontend && npm i && npm start && cd ..",
13 | "backend": "npm run mongo-up && nodemon server.js",
14 | "mongo-up": "cd mongo-replicaset && docker-compose up -d && cd ..",
15 | "mongo-down": "cd mongo-replicaset && docker-compose down && cd ..",
16 | "apidoc": "./node_modules/.bin/apidoc -i routers -o out/"
17 | },
18 | "author": "",
19 | "license": "MIT",
20 | "dependencies": {
21 | "bcryptjs": "^2.4.3",
22 | "dotenv": "^8.2.0",
23 | "express": "^4.17.1",
24 | "express-validator": "^6.6.1",
25 | "mongoose": "^5.10.9",
26 | "mongoose-unique-validator": "^2.0.3"
27 | },
28 | "devDependencies": {
29 | "apidoc": "^0.25.0",
30 | "faker": "^5.1.0",
31 | "jsdoc": "^3.6.6",
32 | "mocha": "^8.2.1",
33 | "nodemon": "^2.0.6",
34 | "request": "^2.88.2",
35 | "supertest": "^6.0.1"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/test/books.test.js:
--------------------------------------------------------------------------------
1 | const request = require('supertest');
2 | const app = require('./../server');
3 | var assert = require('assert');
4 |
5 | /**
6 | * Testing get books
7 | */
8 | describe('GET /api/books', function () {
9 | it('respond with all the books', function (done) {
10 | request(app)
11 | .get('/api/books')
12 | .set('Accept', 'application/json')
13 | .expect('Content-Type', /json/)
14 | .expect(200, done)
15 | });
16 | });
17 |
18 | /**
19 | * Testing get books by id
20 | */
21 | describe('GET /api/books/:id', function () {
22 | it('respond with particular book', function (done) {
23 | request(app)
24 | .get('/api/books/5fcee759a0d5fa8fd21c0146')
25 | .set('Accept', 'application/json')
26 | .expect('Content-Type', /json/)
27 | .end(function(err, response) {
28 | if (err) { return done(err); }
29 | assert.strictEqual(response.status, 200);
30 | assert.strictEqual(response.body.title, 'grey');
31 | done();
32 | });
33 | });
34 | });
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 threedevs
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/frontend/src/pages/Home.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import './home.scss';
3 | import { Link } from 'react-router-dom';
4 |
5 | import { allBooksURL } from '../SERVER';
6 | import BookCard from '../components/BookCard';
7 |
8 | function Section({ title }) {
9 | const [books, setBooks] = useState([]);
10 |
11 | useEffect(() => {
12 | fetch(allBooksURL)
13 | .then((response) => {
14 | if (response.status === 200) {
15 | return response.json();
16 | } else {
17 | return Promise.reject();
18 | }
19 | })
20 | .then((data) => setBooks(data))
21 | .catch((err) => console.error(err));
22 | }, []);
23 |
24 | return (
25 |
26 | {title}
27 |
28 | {books.map((book) => (
29 | -
30 |
31 |
32 | ))}
33 |
34 |
35 | );
36 | }
37 |
38 | export default function Home() {
39 | return (
40 | <>
41 | Welcome home
42 |
43 | >
44 | );
45 | }
46 |
--------------------------------------------------------------------------------
/test/users.test.js:
--------------------------------------------------------------------------------
1 | const request = require('supertest');
2 | const app = require('./../server');
3 | var assert = require('assert');
4 |
5 | /**
6 | * Testing get books
7 | */
8 | describe('GET /api/users/:id', function () {
9 | it('check with invalid user id', function (done) {
10 | request(app)
11 | .get('/api/users/:id')
12 | .set('Accept', 'application/json')
13 | .expect('Content-Type', /json/)
14 | .end(function(err, response) {
15 | if (err) { return done(err); }
16 | assert.strictEqual(response.status, 400);
17 | done();
18 | });
19 | });
20 | });
21 |
22 | /**
23 | * Testing get user by username
24 | */
25 | describe('GET /api/users/s/:search', function () {
26 | it('check with invalid user name', function (done) {
27 | request(app)
28 | .get('/api/users/s/:search')
29 | .set('Accept', 'application/json')
30 | .expect('Content-Type', /json/)
31 | .end(function(err, response) {
32 | if (err) { return done(err); }
33 | assert.strictEqual(response.status, 400);
34 | done();
35 | });
36 | });
37 | });
--------------------------------------------------------------------------------
/frontend/src/App.scss:
--------------------------------------------------------------------------------
1 | @import '~normalize.css';
2 | @import 'fonts/fontWeight';
3 | @import 'fonts/font-face';
4 |
5 | @media (prefers-reduced-motion: reduce) {
6 | *,
7 | *::before,
8 | *::after {
9 | transition: none !important;
10 | animation-duration: 0s !important;
11 | }
12 | }
13 |
14 | :root {
15 | --primary-color: #5ba788;
16 | --nav-color: #323031;
17 | --nav-color-rgb: 50, 48, 49;
18 | --content-color: #3d3b3c;
19 | --error-color: #3d3b3c;
20 | --text-color: #ffffff;
21 |
22 | color: var(--text-color);
23 | font: $regular 1em/1.4 Raleway, sans-serif;
24 | scroll-behavior: smooth;
25 | background: var(--content-color);
26 | }
27 |
28 | *,
29 | *::before,
30 | *::after {
31 | box-sizing: border-box;
32 | }
33 |
34 | body {
35 | display: flex;
36 | flex-direction: column;
37 | min-height: 100vh;
38 | }
39 |
40 | svg,
41 | img,
42 | picture {
43 | display: block;
44 | max-width: 100%;
45 | }
46 |
47 | button {
48 | cursor: pointer;
49 | }
50 |
51 | main {
52 | flex-grow: 1;
53 | padding: 2em;
54 | }
55 |
56 | .readerOnly {
57 | position: absolute;
58 |
59 | width: 1px;
60 | height: 1px;
61 | margin: 0;
62 | overflow: hidden;
63 |
64 | clip: rect(1px, 1px, 1px, 1px);
65 | clip-path: polygon(0 0, 0 0, 0 0);
66 | }
67 |
--------------------------------------------------------------------------------
/frontend/src/components/navigation.scss:
--------------------------------------------------------------------------------
1 | @import '../fonts/fontWeight';
2 |
3 | .Navigation {
4 | position: absolute;
5 | top: 100%;
6 | left: 0;
7 | display: flex;
8 | flex-direction: column;
9 | width: 100%;
10 | height: calc(100vh - 5em);
11 | transform: translate(-100%, 0);
12 | transition: all 0.3s ease;
13 | background: var(--nav-color);
14 |
15 | @supports (backdrop-filter: blur(15px)) {
16 | background: rgba(var(--nav-color-rgb), 0.25);
17 | backdrop-filter: blur(15px);
18 | }
19 |
20 | &__list {
21 | flex-grow: 1;
22 | margin: 0;
23 | padding: 2em;
24 | list-style-type: none;
25 |
26 | > li::before {
27 | display: block;
28 | height: 0;
29 |
30 | content: '\200B';
31 | }
32 | }
33 |
34 | &__item {
35 | display: flex;
36 | align-items: center;
37 | min-height: 2em;
38 | padding-left: 2.5em;
39 |
40 | &:not(:first-of-type) {
41 | margin-top: 1.5em;
42 | }
43 | }
44 |
45 | &__link {
46 | color: currentColor;
47 | font: $regular 1.5em/1.2 'Raleway', sans-serif;
48 | text-decoration: none;
49 |
50 | &:hover,
51 | &:focus {
52 | color: var(--primary-color);
53 | }
54 | }
55 |
56 | &__section {
57 | display: flex;
58 | align-items: center;
59 | min-height: 2em;
60 | padding: 1.5em 0 1.5em 4.5em;
61 | border-top: currentColor 1px solid;
62 | }
63 |
64 | &--open {
65 | transform: translate(0, 0);
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/learning/datastructures/data.js:
--------------------------------------------------------------------------------
1 | const data = [
2 | {
3 | id: 2,
4 | title: 'Inception',
5 | yearstart: 2010,
6 | //here yearend is undefined :(
7 | yearend: undefined,
8 | description: 'Dreamception',
9 | videolink: 'https://www.imdb.com/title/tt1375666/?ref_=fn_al_tt_1',
10 | main_actor: {
11 | name: 'Leonardo Di Caprio',
12 | latest_medias: 'Inception, The Revenant', //the revenant is newer than inception, order from left to right
13 | older_medias: ['Body of Lies', 'The Departed', 'Blood Diamond'],
14 | },
15 | other_actors: [
16 | {
17 | name: 'Ellen Page',
18 | latest_media: 'Unthinkable',
19 | other_media: 'Flatliners',
20 | },
21 | {
22 | name: 'Tom Hardy',
23 | latest_media: 'Peaky Blinders',
24 | other_media: 'RockNRolla',
25 | },
26 | ],
27 | },
28 | {
29 | id: 3,
30 | title: 'The Wire',
31 | yearstart: 2002,
32 | yearend: 2008,
33 | description: 'Baltimore',
34 | //videolink missing
35 | main_actor: {
36 | name: 'Stringer Bell',
37 | latest_medias: 'Three Thousand Years of Longing',
38 | older_medias: ['Luther', 'Peaky Blinders', 'RockNRolla'],
39 | },
40 | other_actors: [
41 | {
42 | name: 'Tony Soprano',
43 | latest_media: 'The Sopranos',
44 | other_media: undefined,
45 | },
46 | {
47 | name: 'Omar Little',
48 | latest_media: 'The Wire',
49 | other_media: undefined,
50 | },
51 | ],
52 | },
53 | ];
54 | export default data;
55 |
--------------------------------------------------------------------------------
/frontend/src/pages/home.scss:
--------------------------------------------------------------------------------
1 | @import '../fonts/fontWeight';
2 |
3 | .PageTitle {
4 | margin: 0 0 1.25em;
5 | font: $regular 2.25em/1.5 'Quicksand', sans-serif;
6 | }
7 |
8 | .Section {
9 | &:not(:last-of-type) {
10 | margin-bottom: 4em;
11 | }
12 |
13 | &__title {
14 | margin: 0 0 1.25em;
15 | font: $regular 1.5em/1.5 'Quicksand', sans-serif;
16 | }
17 |
18 | &__list {
19 | display: grid;
20 | grid-template-columns: repeat(auto-fit, minmax(14em, 1fr));
21 | grid-gap: 2.5em;
22 | width: calc(100% + 2em);
23 | margin: 0;
24 | padding: 0 0 1em;
25 | //overflow: auto;
26 | list-style-type: none;
27 |
28 | > li::before {
29 | display: block;
30 | height: 0;
31 |
32 | content: '\200B';
33 | }
34 | }
35 | }
36 |
37 | .BookCard {
38 | position: relative;
39 | display: flex;
40 | flex-direction: column;
41 | padding: 1em;
42 |
43 | &__title {
44 | font: $regular 1em/1.5 'Raleway', sans-serif;
45 | }
46 |
47 | &__media {
48 | order: -1;
49 | margin: 0 auto;
50 | }
51 |
52 | dl {
53 | display: grid;
54 | grid-template-columns: max-content 1fr;
55 | grid-gap: 0.5em 1em;
56 | align-items: baseline;
57 | margin: 0;
58 | }
59 |
60 | dt {
61 | font: $regular 0.75em/1.5 'Quicksand', sans-serif;
62 | }
63 |
64 | dd {
65 | margin: 0;
66 | font: $regular 0.875em/1.5 'Quicksand', sans-serif;
67 | }
68 |
69 | a {
70 | outline: none;
71 | color: var(--primary-color);
72 |
73 | &::before {
74 | position: absolute;
75 | top: 0;
76 | right: 0;
77 | bottom: 0;
78 | left: 0;
79 | border-radius: .25em;
80 | content: '';
81 | transition: all .3s ease;
82 | }
83 |
84 | &:hover,
85 | &:focus {
86 | &::before {
87 | box-shadow: var(--primary-color) 0 0 .25em .25em;
88 | }
89 | }
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/learning/datastructures/test.js:
--------------------------------------------------------------------------------
1 | import data from './data.js';
2 | import functions from './datastructures.js';
3 | import expected from './expected.js';
4 |
5 | window.addEventListener('load', () => {
6 | const logEl = document.getElementById('log');
7 | const dateEl = document.getElementById('lasttest');
8 | const timeEl = document.getElementById('lttime');
9 | testFunctions();
10 | document.getElementById('measure').addEventListener('click', () => testFunctions());
11 | function testFunctions() {
12 | performance.mark('all');
13 | console.log('START TESTS');
14 | dateEl.innerText = new Date().toString('en-gb');
15 | logEl.innerHTML = '';
16 | Object.getOwnPropertyNames(functions)
17 | .filter((prop) => typeof functions[prop] === 'function' && prop != 'constructor')
18 | .forEach((fname, i) => {
19 | const el = document.createElement('p');
20 | performance.mark('individualtest');
21 | const result = functions[fname](_.cloneDeep(data));
22 | const speed = performance.measure('it', 'individualtest').duration;
23 | if (!compare(result, expected[i])) {
24 | console.log(expected[i]);
25 | console.log(functions[fname](_.cloneDeep(data)));
26 | el.innerText = `FAILED test ${fname} query took ${speed} ms`;
27 | el.style = 'color: red;';
28 | console.error('FAILED ' + fname);
29 | } else {
30 | el.style = 'color: green;';
31 | el.innerText = `PASSED test ${fname} query took ${speed} ms`;
32 | console.log('PASSED ' + fname);
33 | }
34 | logEl.appendChild(el);
35 | });
36 | timeEl.innerText = `all tests took ${performance.measure('it', 'all').duration} ms`;
37 | }
38 |
39 | function compare(result, expected) {
40 | if (result == undefined) {
41 | console.error('undefined!');
42 | return false;
43 | }
44 | return _.isEqual(result, expected);
45 | }
46 | });
47 |
--------------------------------------------------------------------------------
/frontend/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/frontend/src/components/header.scss:
--------------------------------------------------------------------------------
1 | @import '../fonts/fontWeight';
2 |
3 | .Header {
4 | position: sticky;
5 | top: 0;
6 | left: 0;
7 | display: flex;
8 | align-items: center;
9 | height: 5em;
10 | padding: 1em;
11 | background: var(--nav-color);
12 |
13 | &__title {
14 | margin: 0;
15 | font: $light 2em/1 'Quicksand', sans-serif;
16 | }
17 |
18 | &__link {
19 | display: flex;
20 | align-items: center;
21 | color: var(--primary-color);
22 | text-decoration: none;
23 |
24 | svg {
25 | width: 1.2em;
26 | height: 1.2em;
27 | margin-right: 0.5em;
28 | }
29 | }
30 |
31 | &__navBtn {
32 | box-sizing: content-box;
33 | position: fixed;
34 | top: 1.2em;
35 | right: 1.5em;
36 | z-index: 110;
37 | width: 2.25em;
38 | height: 1.25em;
39 | margin: 0;
40 | padding: 0.5em;
41 |
42 | color: currentColor;
43 | background: none;
44 | border: none;
45 | outline: none;
46 | overflow: hidden;
47 |
48 | &:hover,
49 | &:focus {
50 | color: var(--primary-color);
51 | }
52 |
53 | span:first-of-type {
54 | position: absolute;
55 | clip: rect(0 0 0 0);
56 | }
57 |
58 | &Line {
59 | $buttonDiv: &;
60 |
61 | position: absolute;
62 | left: 50%;
63 | transform: translateX(-50%);
64 | width: 1.5em;
65 | height: 3px;
66 | background: currentColor;
67 | transition: all 0.3s ease;
68 |
69 | &--1 {
70 | top: 0.5em;
71 | }
72 |
73 | &--2 {
74 | top: 50%;
75 | transform: translate(-50%, -50%);
76 | }
77 |
78 | &--3 {
79 | bottom: 0.5em;
80 | }
81 | }
82 |
83 | &--toggled {
84 | color: currentColor;
85 |
86 | .Header__navBtnLine {
87 | &--1 {
88 | top: 50%;
89 | transform: translate(-50%) rotateZ(-45deg);
90 | }
91 |
92 | &--2 {
93 | transform: translate(-50%) rotateZ(45deg);
94 | }
95 |
96 | &--3 {
97 | bottom: 0;
98 | opacity: 0;
99 | }
100 | }
101 | }
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/db/mongoose.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 | const schemas = require('./schemas');
3 | const Bcrypt = require('bcryptjs');
4 | const uniqueValidator = require('mongoose-unique-validator');
5 | const faker = require('faker');
6 |
7 | //const mongoURI = 'mongodb://127.0.0.1:27017/library';
8 | const mongoURI = 'mongodb://127.0.0.1:27001, 127.0.0.1:27002, 127.0.0.1:27003/library?replicaSet=mongo-rs&readPreference=primary&ssl=false';
9 |
10 | const optionsMoongoose = {
11 | useCreateIndex: true,
12 | useFindAndModify: false,
13 | useUnifiedTopology: true,
14 | useNewUrlParser: true,
15 | };
16 |
17 | mongoose.connect(mongoURI, optionsMoongoose);
18 |
19 | const userSchema = mongoose.Schema(schemas.user);
20 | userSchema.plugin(uniqueValidator);
21 | userSchema.pre('save', function (next) {
22 | if (!this.isModified('password')) {
23 | return next();
24 | }
25 | this.password = Bcrypt.hashSync(this.password, 10);
26 | next();
27 | });
28 |
29 | const userDoc = mongoose.model('user', userSchema);
30 |
31 | const bookSchema = mongoose.Schema(schemas.book);
32 | const bookDoc = mongoose.model('book', bookSchema);
33 |
34 | mongoose.connection.on('error', (err) => console.error(err));
35 | mongoose.connection.once('open', () => {
36 | (async () => {
37 | /**
38 | * add books if development and none are there
39 | */
40 | if (process.env.NODE_ENV !== 'production' && !(await bookDoc.findOne({}))) {
41 | console.log('no books found in development, trying to add 500');
42 | for (let i = 0; i < 500; i++) {
43 | const resBook = await bookDoc.create({
44 | title: faker.random.word(),
45 | author: faker.name.lastName(),
46 | genre: faker.commerce.department(),
47 | yearPublished: faker.random.number(),
48 | });
49 | if (!resBook) {
50 | throw new Error('failed to add a book');
51 | }
52 | }
53 | }
54 | })().catch((err) => console.error(err));
55 |
56 | console.log('connection established to mongodb');
57 | });
58 |
59 | module.exports = {
60 | db: mongoose,
61 | bookDoc,
62 | userDoc,
63 | };
64 |
--------------------------------------------------------------------------------
/frontend/README.md:
--------------------------------------------------------------------------------
1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
2 |
3 | ## Available Scripts
4 |
5 | In the project directory, you can run:
6 |
7 | ### `yarn start`
8 |
9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
11 |
12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console.
14 |
15 | ### `yarn test`
16 |
17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
19 |
20 | ### `yarn build`
21 |
22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance.
24 |
25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed!
27 |
28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
29 |
30 | ### `yarn eject`
31 |
32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!**
33 |
34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
35 |
36 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
37 |
38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
39 |
40 | ## Learn More
41 |
42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
43 |
44 | To learn React, check out the [React documentation](https://reactjs.org/).
45 |
46 | ### Code Splitting
47 |
48 | This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting
49 |
50 | ### Analyzing the Bundle Size
51 |
52 | This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size
53 |
54 | ### Making a Progressive Web App
55 |
56 | This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app
57 |
58 | ### Advanced Configuration
59 |
60 | This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration
61 |
62 | ### Deployment
63 |
64 | This section has moved here: https://facebook.github.io/create-react-app/docs/deployment
65 |
66 | ### `yarn build` fails to minify
67 |
68 | This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify
69 |
--------------------------------------------------------------------------------
/frontend/public/library_icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
71 |
--------------------------------------------------------------------------------
/learning/datastructures/datastructures.js:
--------------------------------------------------------------------------------
1 | class functions {
2 | /**
3 | * gets the title of the first media
4 | */
5 | static getFirstTitle(media) {
6 | return; //your solution...
7 | }
8 | /**
9 | * gets the first media with id 2
10 | */
11 | static getTitleWithId2(media) {
12 | return; //your solution...
13 | }
14 | /**
15 | * gets all media with yearstart 2010 or earlier
16 | */
17 | static getAllMedia2010Plus(media) {
18 | return; //your solution...
19 | }
20 | /**
21 | * gets the description of the media with id 2
22 | */
23 | static getDescriptionWithId2(media) {
24 | return; //your solution...
25 | }
26 | /**
27 | * gets all media with the title "Inception" or "The Wire"
28 | */
29 | static getAllWithTitleIW(media) {
30 | return; //your solution...
31 | }
32 | /**
33 | * gets the first medias main_actor name
34 | */
35 | static getFirstMainActor(media) {
36 | return; //your solution...
37 | }
38 | /**
39 | * gets the latest medias of the mainactor of the first media with title "Inception"
40 | */
41 | static complexRetrieve1(media) {
42 | return; //your solution...
43 | }
44 | /**
45 | * gets the latest medias of the mainactors of all medias with title "Inception" or "The Wire"
46 | */
47 | static complexRetrieve2(media) {
48 | return; //your solution...
49 | }
50 | /**
51 | * gets the latest media of the first otheractor in the first media that was running in between 2004 and 2006
52 | */
53 | static complexRetrieve3(media) {
54 | return; //your solution...
55 | }
56 | /**
57 | * gets the latest medias of the first otheractor in all medias that were running in between 2000 and 2020
58 | */
59 | static complexRetrieve4(media) {
60 | return; //your solution...
61 | }
62 | /**
63 | * gets all medias that have otheractors or mainactors that participated in the media "Unthinkable"
64 | */
65 | static complexRetrieve5(media) {
66 | return; //your solution...
67 | }
68 | /**
69 | * gets all names of actors(regardless otheractors or mainactors) that have participated in the media "Peaky Blinders"
70 | */
71 | static complexRetrieve6(media) {
72 | return; //your solution...
73 | }
74 | /**
75 | * checks if media only contains objects that are only having unique ids
76 | */
77 | static checkUniqueIds(media) {
78 | return; //your solution...
79 | }
80 | /**
81 | * sanitizes the yearend field to be the number 0 if a falsy value is given (remember what a falsy value is)
82 | */
83 | static sanitizeFalsy(media) {
84 | return; //your solution...
85 | }
86 | /**
87 | * adds a videolink field if its missing, initialize it to an empty string
88 | */
89 | static sanitizeVideoLink(media) {
90 | return; //your solution...
91 | }
92 | /**
93 | * converts the latest_medias field of the mainactor to an array instead of string (each entry in the array is a media)
94 | */
95 | static sanitizeLatestMediaList(media) {
96 | return; //your solution...
97 | }
98 | /**
99 | * returns the media with only the id field and title
100 | */
101 | static reduceToIdAndTitle(media) {
102 | return; //your solution...
103 | }
104 | /**
105 | * adds a date field to all medias, set the date to "2020-10-17"
106 | */
107 | static feedTimeField(media) {
108 | return; //your solution...
109 | }
110 | /**
111 | * returns the media split by its category (movie, TV Show)
112 | * "the yearend is falsy if its a movie"
113 | * an array of arrays is expected
114 | */
115 | static splitByCategory(media) {
116 | return; //your solution...
117 | }
118 | /**
119 | * extracts all medias from every possible field (so f.e. also othermedia in otheractors) and add a field "occurences"
120 | * the title also counts!
121 | * that shows how often this media was represented in the data structure,
122 | * an alphabetically sorted array by media title is expected,
123 | * avoid having undefined in your new array!
124 | * expected is an array of objects
125 | */
126 | static complexCombined1(media) {
127 | return; //your solution...
128 | }
129 | /**
130 | * extracts all actors and add a field "popularity" to each actor that represents how often the actor was represented as well as its role,
131 | * the actor gains 2 popularity if he was a main actor and 1 of he was a "otheractor/sideactor", only return the 3 most popular actors,
132 | * if theres a tie in popularity, you dont need to do something specific
133 | * expected is an array of objects
134 | */
135 | static complexCombined2(media) {
136 | return; //your solution...
137 | }
138 | }
139 | export default functions;
140 |
--------------------------------------------------------------------------------
/routers/users.js:
--------------------------------------------------------------------------------
1 | const userRouter = require('express').Router();
2 | const { param, body, validationResult } = require('express-validator');
3 |
4 | const { userDoc } = require('../db/mongoose');
5 |
6 | /**
7 | * @api {post} /api/users Create a New User.
8 | * @apiName PostUser
9 | * @apiGroup Users
10 | * @apiVersion 0.1.0
11 | *
12 | * @apiSuccess {object} user Newly Created User information.
13 | * @apiSuccess {String} username User's username
14 | *
15 | * @apiSuccessExample {json} Success-Response:
16 | * HTTP/1.1 200 OK
17 | * {
18 | * "username": "John Doe"
19 | * }
20 | */
21 | userRouter.post(
22 | '/',
23 | [
24 | body('username').isString().isLength({ min: 5, max: 100 }),
25 | body('password').isString().isLength({ min: 6, max: 100 }),
26 | body().custom((body) => (Object.keys(body).length < 3 ? Promise.resolve() : Promise.reject())),
27 | ],
28 | async (req, res) => {
29 | try {
30 | console.log('POST');
31 | console.log(req.body);
32 | const errors = validationResult(req);
33 | if (!errors.isEmpty()) {
34 | return res.status(400).json({ errors: errors.array() });
35 | }
36 | //validate every property from start to finish
37 | //here we skipped password hashing and strength
38 | //here we skipped username validation (is string length min max)
39 | const newUser = new userDoc(req.body);
40 | const userRes = await newUser.save();
41 | if (!userRes) {
42 | throw new Error('failed to make user');
43 | }
44 |
45 | userRes.password = undefined;
46 |
47 | return res.json(userRes);
48 | } catch (e) {
49 | console.error(e);
50 | return res.sendStatus(400);
51 | }
52 | }
53 | );
54 |
55 | /**
56 | * @api {get} /api/users/:id Fetch a Single User by its Id.
57 | * @apiName GetUser
58 | * @apiGroup Users
59 | * @apiVersion 0.1.0
60 | *
61 | * @apiParam {String} id Id of the User being fetched.
62 | *
63 | * @apiSuccess {object} user Fetched user's information.
64 | * @apiSuccess {String} username User's username
65 | *
66 | * @apiSuccessExample {json} Success-Response:
67 | * HTTP/1.1 200 OK
68 | * {
69 | * "username": "John Doe"
70 | * }
71 | */
72 |
73 | userRouter.get('/:id', [param('id').isMongoId()], async (req, res) => {
74 | try {
75 | const errors = validationResult(req);
76 |
77 | if (!errors.isEmpty()) {
78 | return res.status(400).json({ errors: errors.array() });
79 | }
80 |
81 | const user = await userDoc.findById(req.params.id);
82 |
83 | if (!user) {
84 | throw new Error(`Can not find a user by this id`);
85 | }
86 |
87 | user.password = undefined;
88 |
89 | return res.json(user);
90 | } catch (e) {
91 | console.error(e);
92 | return res.sendStatus(500);
93 | }
94 | });
95 |
96 | /**
97 | * @api {get} /api/users/s/:search Search for a user with the username
98 | * @apiName SearchUser
99 | * @apiGroup Users
100 | * @apiVersion 0.1.0
101 | *
102 | * @apiSuccess {object} user Fetched user's information
103 | */
104 | userRouter.get('/s/:search',[param('search').isString({ min: 5, max: 100 })], async (req, res) => {
105 | try {
106 | const errors = validationResult(req);
107 |
108 | if (!errors.isEmpty()) {
109 | return res.status(400).json({ errors: errors.array() });
110 | }
111 |
112 | const users = await userDoc.find({username : req.params.search});
113 |
114 | if (!users) {
115 | throw new Error(`Can not find a user by this username`);
116 | }
117 | return res.json(users);
118 | } catch (e) {
119 | console.error(e);
120 | return res.sendStatus(500);
121 | }
122 | });
123 |
124 | /**
125 | * @api {get} /api/users Fetch all the available users
126 | * @apiName GetUser
127 | * @apiGroup Users
128 | * @apiVersion 0.1.0
129 | *
130 | * @apiSuccess {object[]} users List of all users
131 | */
132 | userRouter.get('/', async (req, res) => {
133 | try {
134 | const users = await userDoc.find({});
135 | if (!users) {
136 | throw new Error('No users were found');
137 | }
138 | return res.json(users);
139 | } catch (e) {
140 | console.error(e);
141 | return res.sendStatus(500);
142 | }
143 | });
144 |
145 | /**
146 | * @api {delete} /api/users/:id Delete a User by its Id
147 | * @apiName DeleteUser
148 | * @apiGroup Users
149 | * @apiVersion 0.1.0
150 | *
151 | * @apiParam {String} id Id of the User being deleted.
152 | *
153 | * @apiSuccessExample {json} Success-Response:
154 | * HTTP/1.1 200 OK
155 | */
156 | userRouter.delete('/:id', [param('id').isMongoId()], async (req, res) => {
157 | try {
158 | const errors = validationResult(req);
159 | if (!errors.isEmpty()) {
160 | return res.status(400).json({ errors: errors.array() });
161 | }
162 |
163 | const user = await userDoc.findByIdAndRemove(req.params.id);
164 |
165 | if (!user) {
166 | return res.sendStatus(404);
167 | }
168 |
169 | res.sendStatus(200);
170 | } catch (e) {
171 | console.error(e);
172 | return res.sendStatus(500);
173 | }
174 | });
175 |
176 |
177 | module.exports = userRouter
178 |
--------------------------------------------------------------------------------
/frontend/src/serviceWorker.js:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read https://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.0/8 are considered localhost for IPv4.
18 | window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/)
19 | );
20 |
21 | export function register(config) {
22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
23 | // The URL constructor is available in all browsers that support SW.
24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
25 | if (publicUrl.origin !== window.location.origin) {
26 | // Our service worker won't work if PUBLIC_URL is on a different origin
27 | // from what our page is served on. This might happen if a CDN is used to
28 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
29 | return;
30 | }
31 |
32 | window.addEventListener('load', () => {
33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
34 |
35 | if (isLocalhost) {
36 | // This is running on localhost. Let's check if a service worker still exists or not.
37 | checkValidServiceWorker(swUrl, config);
38 |
39 | // Add some additional logging to localhost, pointing developers to the
40 | // service worker/PWA documentation.
41 | navigator.serviceWorker.ready.then(() => {
42 | console.log('This web app is being served cache-first by a service ' + 'worker. To learn more, visit https://bit.ly/CRA-PWA');
43 | });
44 | } else {
45 | // Is not localhost. Just register service worker
46 | registerValidSW(swUrl, config);
47 | }
48 | });
49 | }
50 | }
51 |
52 | function registerValidSW(swUrl, config) {
53 | navigator.serviceWorker
54 | .register(swUrl)
55 | .then((registration) => {
56 | registration.onupdatefound = () => {
57 | const installingWorker = registration.installing;
58 | if (installingWorker == null) {
59 | return;
60 | }
61 | installingWorker.onstatechange = () => {
62 | if (installingWorker.state === 'installed') {
63 | if (navigator.serviceWorker.controller) {
64 | // At this point, the updated precached content has been fetched,
65 | // but the previous service worker will still serve the older
66 | // content until all client tabs are closed.
67 | console.log(
68 | 'New content is available and will be used when all ' + 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
69 | );
70 |
71 | // Execute callback
72 | if (config && config.onUpdate) {
73 | config.onUpdate(registration);
74 | }
75 | } else {
76 | // At this point, everything has been precached.
77 | // It's the perfect time to display a
78 | // "Content is cached for offline use." message.
79 | console.log('Content is cached for offline use.');
80 |
81 | // Execute callback
82 | if (config && config.onSuccess) {
83 | config.onSuccess(registration);
84 | }
85 | }
86 | }
87 | };
88 | };
89 | })
90 | .catch((error) => {
91 | console.error('Error during service worker registration:', error);
92 | });
93 | }
94 |
95 | function checkValidServiceWorker(swUrl, config) {
96 | // Check if the service worker can be found. If it can't reload the page.
97 | fetch(swUrl, {
98 | headers: { 'Service-Worker': 'script' },
99 | })
100 | .then((response) => {
101 | // Ensure service worker exists, and that we really are getting a JS file.
102 | const contentType = response.headers.get('content-type');
103 | if (response.status === 404 || (contentType != null && contentType.indexOf('javascript') === -1)) {
104 | // No service worker found. Probably a different app. Reload the page.
105 | navigator.serviceWorker.ready.then((registration) => {
106 | registration.unregister().then(() => {
107 | window.location.reload();
108 | });
109 | });
110 | } else {
111 | // Service worker found. Proceed as normal.
112 | registerValidSW(swUrl, config);
113 | }
114 | })
115 | .catch(() => {
116 | console.log('No internet connection found. App is running in offline mode.');
117 | });
118 | }
119 |
120 | export function unregister() {
121 | if ('serviceWorker' in navigator) {
122 | navigator.serviceWorker.ready
123 | .then((registration) => {
124 | registration.unregister();
125 | })
126 | .catch((error) => {
127 | console.error(error.message);
128 | });
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 3dev-fullstack, learn fullstack! (MERN Stack)
2 | [](https://github.com/threedevs/3dev-fullstack/blob/master/LICENSE)
3 | [](https://GitHub.com/Naereen/StrapDown.js/graphs/commit-activity)
4 | [](https://GitHub.com/threedevs/3dev-fullstack/issues)
5 | [](https://GitHub.com/threedevs/3dev-fullstack/issues?q=is%3Aissue+is%3Aclosed)
6 | [](https://GitHub.com/threedevs/3dev-fullstack/pull/)
7 | [](http://makeapullrequest.com)
8 | 
9 | ## technology stack
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | ## current state
31 | 
32 | ## goal of this repository (vision)
33 | create a plex-like book library, with multiple books and user authentication, with the addition of having anki cards and even more to come...
34 | 
35 | ## contribute
36 | If you have spare time, you can always make an addition to the system! If you have something different in mind, just DM @kwlski on DC. There is a list of issues to pick from.
37 | ### how to
38 | - implement one of the possible issues, assign the issue to yourself, if you are a beginner I can help you getting your changes to github
39 | - push your feature to a non protected branch, hereby we name the branch after the task it fulfills, in small letters. For example I make a documentation file for routes, so we are going to name the branch "routesdoc"
40 | - open a pullrequest on github, that can be reviewed and merged into the "master" branch
41 | - done! You have contributed!
42 | ### prototype
43 | to contribute, you will need to have an idea what the app should look like, for this please see the prototype:
44 | - **[in work, see the prototype of the project! [Issue #2](https://github.com/threedevs/3dev-fullstack/issues/2)]**
45 | ## how to use the repository
46 | ### 1. first you will need to install git, node and docker
47 | ```
48 | https://git-scm.com/downloads
49 | https://nodejs.org/en/
50 | https://www.docker.com/products/docker-desktop
51 | ```
52 | ### 2. clone the repository
53 | now you can use git to clone this repository and get it so you have it locally on your machine as physical files
54 | for this go to your desired directory where you want to have your files and run the following command
55 | ```
56 | git clone https://github.com/threedevs/3dev-fullstack.git
57 | ```
58 | you should now see a folder 3dev-fullstack
59 |
60 | ### 3. install the dependencies
61 | open the folder 3dev-fullstack and install the packages that we need with following command from the npm registry
62 | ```
63 | npm i
64 | ```
65 | ### 4. run the database and the server for development
66 |
67 | now you should be able to run the node server with nodemon which keeps track of changes and restarts the server if needed
68 |
69 | ```
70 | npm run backend
71 | ```
72 |
73 | ### 5. run the frontend
74 | for this you can run in a separate terminal
75 | ```
76 | npm run frontend
77 | ```
78 | ### 6. done!
79 |
80 | ## additional commands
81 | start the db
82 | ```
83 | npm run mongo-up
84 | ```
85 | stop the db
86 | ```
87 | npm run mongo-down
88 | ```
89 | ## code documentation
90 | ### Prototype
91 | ```
92 | https://www.figma.com/file/XIUfEV7DlJHJCxwN8BAz5U/Library?node-id=1%3A3
93 | ```
94 | ### API routes
95 | - for this, please see routes.md in the documentation folder
96 | ### API Docs
97 | - generate api documentation to the "out" folder
98 | ```
99 | npm run apidoc
100 | ```
101 | - to generate API documentation we use APIDocs:
102 | ```
103 | ./node_modules/.bin/apidoc -i routers -o out/
104 | ```
105 | - By using the command above in the terminal a new directory called "out" will be created in the project folder, inside this directory open the "index.html" file to view the full documentation. Note if you chose to name the output directory other than "out" please include this in the ".gitignore" file.
106 |
--------------------------------------------------------------------------------
/routers/books.js:
--------------------------------------------------------------------------------
1 | const bookRouter = require('express').Router();
2 | const { bookDoc } = require('../db/mongoose');
3 | const { param, body, validationResult } = require('express-validator');
4 |
5 | /**
6 | * @api {get} /api/books Fetch all the available books.
7 | * @apiName GetBook
8 | * @apiGroup Books
9 | * @apiVersion 0.1.0
10 | *
11 | * @apiSuccess {object[]} books List of all books.
12 | * @apiSuccess {String} title Title of the book.
13 | * @apiSuccess {String} author Author of the book.
14 | * @apiSuccess {String} genre Genre of the book.
15 | * @apiSuccess {String} yearPublished Publication Year.
16 | * @apiSuccess {Date} dateAdded Date at which the book was added
17 | *
18 | * @apiSuccessExample {json} Success-Response:
19 | * HTTP/1.1 200 OK
20 | * [
21 | * {
22 | * "title": "A very cool book",
23 | * "author": "A very cool author",
24 | * "genre": "Not so cool genre",
25 | * "yearPublished": "2019",
26 | * "dateAdded": "22-05-2019"
27 | * }
28 | * ]
29 | */
30 | bookRouter.get('/', async (req, res) => {
31 | try {
32 | const books = await bookDoc.find({});
33 | if (!books) {
34 | throw new Error('No books were found');
35 | }
36 | return res.json(books);
37 | } catch (e) {
38 | console.error(e);
39 | return res.sendStatus(500);
40 | }
41 | });
42 |
43 | /**
44 | * @api {get} /api/books/:id Fetch a Single Book by its Id.
45 | * @apiName GetBook
46 | * @apiGroup Books
47 | * @apiVersion 0.1.0
48 | *
49 | * @apiParam {String} id Id of the Book being fetched.
50 | *
51 | * @apiSuccess {object} book A single Book.
52 | * @apiSuccess {String} title Title of the book.
53 | * @apiSuccess {String} author Author of the book.
54 | * @apiSuccess {String} genre Genre of the book.
55 | * @apiSuccess {String} yearPublished Publication Year.
56 | * @apiSuccess {Data} dateAdded Date at which the book was added
57 | *
58 | * @apiSuccessExample {json} Success-Response:
59 | * HTTP/1.1 200 OK
60 | * {
61 | * "title": "A very cool book",
62 | * "author": "A very cool author",
63 | * "genre": "Not so cool genre",
64 | * "yearPublished": "2019",
65 | * "dateAdded": "22-05-2019"
66 | * }
67 | */
68 | bookRouter.get('/:id', [param('id').isMongoId()], async (req, res) => {
69 | try {
70 | const errors = validationResult(req);
71 |
72 | if (!errors.isEmpty()) {
73 | return res.status(400).json({ errors: errors.array() });
74 | }
75 |
76 | const book = await bookDoc.findById(req.params.id);
77 |
78 | if (!book) {
79 | throw new Error(`Can not find a book by this id`);
80 | }
81 |
82 | return res.json(book);
83 | } catch (e) {
84 | console.error(e);
85 | return res.sendStatus(500);
86 | }
87 | });
88 |
89 | /**
90 | * @api {get} /api/books/s/:search Search all the books with the title.
91 | * @apiName SearchBookByTitle
92 | * @apiGroup Books
93 | * @apiVersion 0.1.0
94 | *
95 | * @apiSuccess {object[]} books List of all books.
96 | * @apiSuccess {String} title Title of the book.
97 | * @apiSuccess {String} author Author of the book.
98 | * @apiSuccess {String} genre Genre of the book.
99 | * @apiSuccess {String} yearPublished Publication Year.
100 | * @apiSuccess {Date} dateAdded Date at which the book was added
101 | *
102 | * @apiSuccessExample {json} Success-Response:
103 | * HTTP/1.1 200 OK
104 | * [
105 | * {
106 | * "title": "A very cool book",
107 | * "author": "A very cool author",
108 | * "genre": "Not so cool genre",
109 | * "yearPublished": "2019",
110 | * "dateAdded": "22-05-2019"
111 | * }
112 | * ]
113 | */
114 | /** search multiple by title */
115 | bookRouter.get('/s/:search',[param('search').isString({ min: 1, max: 100 })], async (req, res) => {
116 | try {
117 | const errors = validationResult(req);
118 |
119 | if (!errors.isEmpty()) {
120 | return res.status(400).json({ errors: errors.array() });
121 | }
122 |
123 | const books = await bookDoc.find({title : req.params.search});
124 |
125 | if (!books) {
126 | throw new Error(`Can not find a books by this search term`);
127 | }
128 |
129 | return res.json(books);
130 | } catch (e) {
131 | console.error(e);
132 | return res.sendStatus(500);
133 | }
134 | });
135 |
136 | /**
137 | * @api {put} /api/books/:id Update a Single Book by its Id.
138 | * @apiName PutBook
139 | * @apiGroup Books
140 | * @apiVersion 0.1.0
141 | *
142 | * @apiParam {String} id Id of the Book being updated.
143 | *
144 | * @apiSuccess {object} book A single Book.
145 | * @apiSuccess {String} title Title of the book.
146 | * @apiSuccess {String} author Author of the book.
147 | * @apiSuccess {String} genre Genre of the book.
148 | * @apiSuccess {String} yearPublished Publication Year.
149 | * @apiSuccess {Date} dateAdded Date at which the book was added.
150 | *
151 | * @apiSuccessExample {json} Success-Response:
152 | * HTTP/1.1 200 OK
153 | * {
154 | * "title": "A very cool book",
155 | * "author": "A very cool author",
156 | * "genre": "Not so cool genre",
157 | * "yearPublished": "2019",
158 | * "dateAdded": "22-05-2019"
159 | * }
160 | */
161 | bookRouter.put(
162 | '/:id',
163 | [
164 | param('id').isMongoId(),
165 | body('title').isString({ min: 1, max: 100 }),
166 | body('author').isString({ min: 1, max: 100 }),
167 | body('genre').isString({ min: 1, max: 100 }),
168 | body('yearPublished').isString({ min: 4, max: 4 }),
169 | body().custom((body) => (Object.keys(body).length < 5 ? Promise.resolve() : Promise.reject())),
170 | ],
171 | async (req, res) => {
172 | try {
173 | const errors = validationResult(req);
174 |
175 | if (!errors.isEmpty()) {
176 | return res.status(400).json({ errors: errors.array() });
177 | }
178 |
179 | const book = await bookDoc.findById(req.params.id);
180 |
181 | if (!book) {
182 | throw new Error('Can not find book by that id');
183 | }
184 |
185 | const body = req.body;
186 |
187 | const newBook = {
188 | title: body.title,
189 | author: body.author,
190 | genre: body.genre,
191 | yearPublished: body.yearPublished,
192 | };
193 |
194 | const updatedBook = await bookDoc.findByIdAndUpdate(req.params.id, newBook, { new: true, runValidators: true });
195 |
196 | if (!updatedBook) {
197 | throw new Error('Unable to update the book');
198 | }
199 |
200 | return res.json(updatedBook);
201 | } catch (e) {
202 | console.error(e);
203 | return res.sendStatus(500);
204 | }
205 | }
206 | );
207 |
208 | /**
209 | * @api {delete} /api/books/:id Delete a Single Book by its Id.
210 | * @apiName DeleteBook
211 | * @apiGroup Books
212 | * @apiVersion 0.1.0
213 | *
214 | * @apiParam {String} id Id of the Book being deleted.
215 | *
216 | * @apiSuccessExample {json} Success-Response:
217 | * HTTP/1.1 200 OK
218 | */
219 | bookRouter.delete('/:id', [param('id').isMongoId()], async (req, res) => {
220 | try {
221 | const errors = validationResult(req);
222 | if (!errors.isEmpty()) {
223 | return res.status(400).json({ errors: errors.array() });
224 | }
225 |
226 | const book = await bookDoc.findByIdAndRemove(req.params.id);
227 |
228 | if (!book) {
229 | return res.sendStatus(404);
230 | }
231 |
232 | res.sendStatus(200);
233 | } catch (e) {
234 | console.error(e);
235 | return res.sendStatus(500);
236 | }
237 | });
238 |
239 | /**
240 | * @api {post} /api/books Create a New Book.
241 | * @apiName PostBook
242 | * @apiGroup Books
243 | * @apiVersion 0.1.0
244 | *
245 | * @apiSuccess {object} book Newly created Book.
246 | * @apiSuccess {String} title Title of the book.
247 | * @apiSuccess {String} author Author of the book.
248 | * @apiSuccess {String} genre Genre of the book.
249 | * @apiSuccess {String} yearPublished Publication Year.
250 | * @apiSuccess {Date} dateAdded Date at which the book was added.
251 | *
252 | * @apiSuccessExample {json} Success-Response:
253 | * HTTP/1.1 200 OK
254 | * {
255 | * "title": "A very cool book",
256 | * "author": "A very cool author",
257 | * "genre": "Not so cool genre",
258 | * "yearPublished": "2019",
259 | * "dateAdded": "22-05-2019"
260 | * }
261 | */
262 | bookRouter.post(
263 | '/',
264 | [
265 | body('title').isString({ min: 1, max: 100 }),
266 | body('author').isString({ min: 1, max: 100 }),
267 | body('genre').isString({ min: 1, max: 100 }),
268 | body('yearPublished').isString({ min: 5, max: 5 }),
269 | body().custom((body) => (Object.keys(body).length < 5 ? Promise.resolve() : Promise.reject())),
270 | ],
271 | async (req, res) => {
272 | try {
273 | const errors = validationResult(req);
274 |
275 | if (!errors.isEmpty()) {
276 | return res.status(400).json({ errors: errors.array() });
277 | }
278 |
279 | const body = req.body;
280 |
281 | const newBook = new bookDoc({
282 | title: body.title,
283 | author: body.author,
284 | genre: body.genre,
285 | yearPublished: body.yearPublished,
286 | dateAdded: new Date(),
287 | });
288 |
289 | const bookRes = await newBook.save();
290 |
291 | if (!bookRes) {
292 | throw new Error('failed to make book');
293 | }
294 |
295 | return res.json(bookRes);
296 | } catch (e) {
297 | console.error(e);
298 | res.sendStatus(500);
299 | }
300 | }
301 | );
302 | module.exports = bookRouter;
303 |
--------------------------------------------------------------------------------
/learning/datastructures/expected.js:
--------------------------------------------------------------------------------
1 | const expected = [
2 | //gets the title of the first media
3 | 'Inception',
4 | //gets the first media with id 2
5 | {
6 | id: 2,
7 | title: 'Inception',
8 | yearstart: 2010,
9 | //here yearend is undefined :(
10 | yearend: undefined,
11 | description: 'Dreamception',
12 | videolink: 'https://www.imdb.com/title/tt1375666/?ref_=fn_al_tt_1',
13 | main_actor: {
14 | name: 'Leonardo Di Caprio',
15 | latest_medias: 'Inception, The Revenant', //the revenant is newer than inception, order from left to right
16 | older_medias: ['Body of Lies', 'The Departed', 'Blood Diamond'],
17 | },
18 | other_actors: [
19 | {
20 | name: 'Ellen Page',
21 | latest_media: 'Unthinkable',
22 | other_media: 'Flatliners',
23 | },
24 | {
25 | name: 'Tom Hardy',
26 | latest_media: 'Peaky Blinders',
27 | other_media: 'RockNRolla',
28 | },
29 | ],
30 | },
31 | //gets all media with yearstart 2010 or earlier
32 | [
33 | {
34 | id: 2,
35 | title: 'Inception',
36 | yearstart: 2010,
37 | //here yearend is undefined :(
38 | yearend: undefined,
39 | description: 'Dreamception',
40 | videolink: 'https://www.imdb.com/title/tt1375666/?ref_=fn_al_tt_1',
41 | main_actor: {
42 | name: 'Leonardo Di Caprio',
43 | latest_medias: 'Inception, The Revenant', //the revenant is newer than inception, order from left to right
44 | older_medias: ['Body of Lies', 'The Departed', 'Blood Diamond'],
45 | },
46 | other_actors: [
47 | {
48 | name: 'Ellen Page',
49 | latest_media: 'Unthinkable',
50 | other_media: 'Flatliners',
51 | },
52 | {
53 | name: 'Tom Hardy',
54 | latest_media: 'Peaky Blinders',
55 | other_media: 'RockNRolla',
56 | },
57 | ],
58 | },
59 | ],
60 | //gets the description of the media with id 2
61 | 'Dreamception',
62 | //gets all media with the title "Inception" or "The Wire"
63 | [
64 | {
65 | id: 2,
66 | title: 'Inception',
67 | yearstart: 2010,
68 | //here yearend is undefined :(
69 | yearend: undefined,
70 | description: 'Dreamception',
71 | videolink: 'https://www.imdb.com/title/tt1375666/?ref_=fn_al_tt_1',
72 | main_actor: {
73 | name: 'Leonardo Di Caprio',
74 | latest_medias: 'Inception, The Revenant', //the revenant is newer than inception, order from left to right
75 | older_medias: ['Body of Lies', 'The Departed', 'Blood Diamond'],
76 | },
77 | other_actors: [
78 | {
79 | name: 'Ellen Page',
80 | latest_media: 'Unthinkable',
81 | other_media: 'Flatliners',
82 | },
83 | {
84 | name: 'Tom Hardy',
85 | latest_media: 'Peaky Blinders',
86 | other_media: 'RockNRolla',
87 | },
88 | ],
89 | },
90 | {
91 | id: 3,
92 | title: 'The Wire',
93 | yearstart: 2002,
94 | yearend: 2008,
95 | description: 'Baltimore',
96 | //videolink missing
97 | main_actor: {
98 | name: 'Stringer Bell',
99 | latest_medias: 'Three Thousand Years of Longing',
100 | older_medias: ['Luther', 'Peaky Blinders', 'RockNRolla'],
101 | },
102 | other_actors: [
103 | {
104 | name: 'Tony Soprano',
105 | latest_media: 'The Sopranos',
106 | other_media: undefined,
107 | },
108 | {
109 | name: 'Omar Little',
110 | latest_media: 'The Wire',
111 | other_media: undefined,
112 | },
113 | ],
114 | },
115 | ],
116 | //gets the first medias main_actor name
117 | 'Leonardo Di Caprio',
118 | //gets the latest medias of the mainactor of the first media
119 | //with title "Inception"
120 | 'Inception, The Revenant',
121 | /**
122 | * gets the latest media of the mainactor of the first media with title "Inception"
123 | */
124 | ['Inception, The Revenant', 'Three Thousand Years of Longing'],
125 | /**
126 | * gets the latest media of the first otheractor in the first media that was running in between 2004 and 2006
127 | */
128 | 'The Sopranos',
129 | /**
130 | * gets the latest medias of the first otheractor in all medias that were running in between 2000 and 2020
131 | */
132 | ['The Sopranos'],
133 | /**
134 | * gets all media that have otheractors or mainactors that participated in the media "Unthinkable"
135 | */
136 | [
137 | {
138 | id: 2,
139 | title: 'Inception',
140 | yearstart: 2010,
141 | //here yearend is undefined :(
142 | yearend: undefined,
143 | description: 'Dreamception',
144 | videolink: 'https://www.imdb.com/title/tt1375666/?ref_=fn_al_tt_1',
145 | main_actor: {
146 | name: 'Leonardo Di Caprio',
147 | latest_medias: 'Inception, The Revenant', //the revenant is newer than inception, order from left to right
148 | older_medias: ['Body of Lies', 'The Departed', 'Blood Diamond'],
149 | },
150 | other_actors: [
151 | {
152 | name: 'Ellen Page',
153 | latest_media: 'Unthinkable',
154 | other_media: 'Flatliners',
155 | },
156 | {
157 | name: 'Tom Hardy',
158 | latest_media: 'Peaky Blinders',
159 | other_media: 'RockNRolla',
160 | },
161 | ],
162 | },
163 | ],
164 | /**
165 | * gets all names of actors(regardless otheractors or mainactors) that have participated in the media "Peaky Blinders"
166 | */
167 | ['Tom Hardy', 'Stringer Bell'],
168 | /**
169 | * checks if media only contains objects that are only having unique ids
170 | */
171 | true,
172 | /**
173 | * sanitizes the yearend field to be the number 0 if a falsy value is given (remember what a falsy value is)
174 | */
175 | [
176 | {
177 | id: 2,
178 | title: 'Inception',
179 | yearstart: 2010,
180 | //here yearend is undefined :(
181 | yearend: 0,
182 | description: 'Dreamception',
183 | videolink: 'https://www.imdb.com/title/tt1375666/?ref_=fn_al_tt_1',
184 | main_actor: {
185 | name: 'Leonardo Di Caprio',
186 | latest_medias: 'Inception, The Revenant', //the revenant is newer than inception, order from left to right
187 | older_medias: ['Body of Lies', 'The Departed', 'Blood Diamond'],
188 | },
189 | other_actors: [
190 | {
191 | name: 'Ellen Page',
192 | latest_media: 'Unthinkable',
193 | other_media: 'Flatliners',
194 | },
195 | {
196 | name: 'Tom Hardy',
197 | latest_media: 'Peaky Blinders',
198 | other_media: 'RockNRolla',
199 | },
200 | ],
201 | },
202 | {
203 | id: 3,
204 | title: 'The Wire',
205 | yearstart: 2002,
206 | yearend: 2008,
207 | description: 'Baltimore',
208 | //videolink missing
209 | main_actor: {
210 | name: 'Stringer Bell',
211 | latest_medias: 'Three Thousand Years of Longing',
212 | older_medias: ['Luther', 'Peaky Blinders', 'RockNRolla'],
213 | },
214 | other_actors: [
215 | {
216 | name: 'Tony Soprano',
217 | latest_media: 'The Sopranos',
218 | other_media: undefined,
219 | },
220 | {
221 | name: 'Omar Little',
222 | latest_media: 'The Wire',
223 | other_media: undefined,
224 | },
225 | ],
226 | },
227 | ],
228 | /**
229 | * adds a videolink field if its missing, initialize it to an empty string
230 | */
231 | [
232 | {
233 | id: 2,
234 | title: 'Inception',
235 | yearstart: 2010,
236 | //here yearend is undefined :(
237 | yearend: undefined,
238 | description: 'Dreamception',
239 | videolink: 'https://www.imdb.com/title/tt1375666/?ref_=fn_al_tt_1',
240 | main_actor: {
241 | name: 'Leonardo Di Caprio',
242 | latest_medias: 'Inception, The Revenant', //the revenant is newer than inception, order from left to right
243 | older_medias: ['Body of Lies', 'The Departed', 'Blood Diamond'],
244 | },
245 | other_actors: [
246 | {
247 | name: 'Ellen Page',
248 | latest_media: 'Unthinkable',
249 | other_media: 'Flatliners',
250 | },
251 | {
252 | name: 'Tom Hardy',
253 | latest_media: 'Peaky Blinders',
254 | other_media: 'RockNRolla',
255 | },
256 | ],
257 | },
258 | {
259 | id: 3,
260 | title: 'The Wire',
261 | yearstart: 2002,
262 | yearend: 2008,
263 | description: 'Baltimore',
264 | videolink: '',
265 | main_actor: {
266 | name: 'Stringer Bell',
267 | latest_medias: 'Three Thousand Years of Longing',
268 | older_medias: ['Luther', 'Peaky Blinders', 'RockNRolla'],
269 | },
270 | other_actors: [
271 | {
272 | name: 'Tony Soprano',
273 | latest_media: 'The Sopranos',
274 | other_media: undefined,
275 | },
276 | {
277 | name: 'Omar Little',
278 | latest_media: 'The Wire',
279 | other_media: undefined,
280 | },
281 | ],
282 | },
283 | ],
284 | /**
285 | * converts the latest_medias field of the mainactor to an array instead of string (each entry in the array is a media)
286 | */
287 | [
288 | {
289 | id: 2,
290 | title: 'Inception',
291 | yearstart: 2010,
292 | //here yearend is undefined :(
293 | yearend: undefined,
294 | description: 'Dreamception',
295 | videolink: 'https://www.imdb.com/title/tt1375666/?ref_=fn_al_tt_1',
296 | main_actor: {
297 | name: 'Leonardo Di Caprio',
298 | latest_medias: ['Inception', 'The Revenant'], //the revenant is newer than inception, order from left to right
299 | older_medias: ['Body of Lies', 'The Departed', 'Blood Diamond'],
300 | },
301 | other_actors: [
302 | {
303 | name: 'Ellen Page',
304 | latest_media: 'Unthinkable',
305 | other_media: 'Flatliners',
306 | },
307 | {
308 | name: 'Tom Hardy',
309 | latest_media: 'Peaky Blinders',
310 | other_media: 'RockNRolla',
311 | },
312 | ],
313 | },
314 | {
315 | id: 3,
316 | title: 'The Wire',
317 | yearstart: 2002,
318 | yearend: 2008,
319 | description: 'Baltimore',
320 | main_actor: {
321 | name: 'Stringer Bell',
322 | latest_medias: ['Three Thousand Years of Longing'],
323 | older_medias: ['Luther', 'Peaky Blinders', 'RockNRolla'],
324 | },
325 | other_actors: [
326 | {
327 | name: 'Tony Soprano',
328 | latest_media: 'The Sopranos',
329 | other_media: undefined,
330 | },
331 | {
332 | name: 'Omar Little',
333 | latest_media: 'The Wire',
334 | other_media: undefined,
335 | },
336 | ],
337 | },
338 | ],
339 | /**
340 | * returns the media with only the id field and title
341 | */
342 | [
343 | {
344 | id: 2,
345 | title: 'Inception',
346 | },
347 | {
348 | id: 3,
349 | title: 'The Wire',
350 | },
351 | ],
352 | /**
353 | * adds a date field to all medias, set the date to "2020-10-17"
354 | */
355 | [
356 | {
357 | id: 2,
358 | title: 'Inception',
359 | yearstart: 2010,
360 | //here yearend is undefined :(
361 | yearend: undefined,
362 | description: 'Dreamception',
363 | videolink: 'https://www.imdb.com/title/tt1375666/?ref_=fn_al_tt_1',
364 | main_actor: {
365 | name: 'Leonardo Di Caprio',
366 | latest_medias: 'Inception, The Revenant', //the revenant is newer than inception, order from left to right
367 | older_medias: ['Body of Lies', 'The Departed', 'Blood Diamond'],
368 | },
369 | other_actors: [
370 | {
371 | name: 'Ellen Page',
372 | latest_media: 'Unthinkable',
373 | other_media: 'Flatliners',
374 | },
375 | {
376 | name: 'Tom Hardy',
377 | latest_media: 'Peaky Blinders',
378 | other_media: 'RockNRolla',
379 | },
380 | ],
381 | date: '2020-10-17',
382 | },
383 | {
384 | id: 3,
385 | title: 'The Wire',
386 | yearstart: 2002,
387 | yearend: 2008,
388 | description: 'Baltimore',
389 | //videolink missing
390 | main_actor: {
391 | name: 'Stringer Bell',
392 | latest_medias: 'Three Thousand Years of Longing',
393 | older_medias: ['Luther', 'Peaky Blinders', 'RockNRolla'],
394 | },
395 | other_actors: [
396 | {
397 | name: 'Tony Soprano',
398 | latest_media: 'The Sopranos',
399 | other_media: undefined,
400 | },
401 | {
402 | name: 'Omar Little',
403 | latest_media: 'The Wire',
404 | other_media: undefined,
405 | },
406 | ],
407 | date: '2020-10-17',
408 | },
409 | ],
410 | /**
411 | * returns the media split by its category (movie, TV Show)
412 | * an array of arrays is expected, first are the movies
413 | */
414 | [
415 | [
416 | {
417 | id: 2,
418 | title: 'Inception',
419 | yearstart: 2010,
420 | //here yearend is undefined :(
421 | yearend: undefined,
422 | description: 'Dreamception',
423 | videolink: 'https://www.imdb.com/title/tt1375666/?ref_=fn_al_tt_1',
424 | main_actor: {
425 | name: 'Leonardo Di Caprio',
426 | latest_medias: 'Inception, The Revenant', //the revenant is newer than inception, order from left to right
427 | older_medias: ['Body of Lies', 'The Departed', 'Blood Diamond'],
428 | },
429 | other_actors: [
430 | {
431 | name: 'Ellen Page',
432 | latest_media: 'Unthinkable',
433 | other_media: 'Flatliners',
434 | },
435 | {
436 | name: 'Tom Hardy',
437 | latest_media: 'Peaky Blinders',
438 | other_media: 'RockNRolla',
439 | },
440 | ],
441 | },
442 | ],
443 | [
444 | {
445 | id: 3,
446 | title: 'The Wire',
447 | yearstart: 2002,
448 | yearend: 2008,
449 | description: 'Baltimore',
450 | //videolink missing
451 | main_actor: {
452 | name: 'Stringer Bell',
453 | latest_medias: 'Three Thousand Years of Longing',
454 | older_medias: ['Luther', 'Peaky Blinders', 'RockNRolla'],
455 | },
456 | other_actors: [
457 | {
458 | name: 'Tony Soprano',
459 | latest_media: 'The Sopranos',
460 | other_media: undefined,
461 | },
462 | {
463 | name: 'Omar Little',
464 | latest_media: 'The Wire',
465 | other_media: undefined,
466 | },
467 | ],
468 | },
469 | ],
470 | ],
471 | /**
472 | * extracts all medias from every possible field (so f.e. also othermedia in otheractors) and add a field "occurences"
473 | * the title also counts!
474 | * that represents how often this media was represented in the data structure,
475 | * an alphabetically sorted array by media title is expected,
476 | * expected is an array of objects
477 | */
478 | [
479 | { title: 'Blood Diamond', occurences: 1 },
480 | { title: 'Body of Lies', occurences: 1 },
481 | { title: 'Flatliners', occurences: 1 },
482 | { title: 'Inception', occurences: 2 },
483 | { title: 'Luther', occurences: 1 },
484 | { title: 'Peaky Blinders', occurences: 2 },
485 | { title: 'RockNRolla', occurences: 2 },
486 | { title: 'The Departed', occurences: 1 },
487 | { title: 'The Revenant', occurences: 1 },
488 | { title: 'The Sopranos', occurences: 1 },
489 | { title: 'The Wire', occurences: 1 },
490 | { title: 'Three Thousand Years of Longing', occurences: 1 },
491 | { title: 'Unthinkable', occurences: 1 },
492 | ],
493 | /**
494 | * extracts all actors and add a field "popularity" to each actor that represents how often the actor was represented as well as its role,
495 | * the actor gains 2 popularity if he was a main actor and 1 of he was a "otheractor/sideactor", only return the 3 most popular actors,
496 | * if theres a tie in popularity, randomize the selected actor for top 3,
497 | * expected is an array of objects
498 | */
499 | [
500 | { actor: 'Stringer Bell', popularity: 2 },
501 | { actor: 'Leonardo Di Caprio', popularity: 2 },
502 | { actor: 'Omar Little', popularity: 1 },
503 | ],
504 | ];
505 | export default expected;
506 |
--------------------------------------------------------------------------------
/frontend/public/library_logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
399 |
--------------------------------------------------------------------------------
/frontend/src/library_logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
399 |
--------------------------------------------------------------------------------