├── .gitignore ├── client ├── public │ ├── Avatars │ │ ├── Wolf.jfif │ │ ├── Kredam.jfif │ │ └── Selim.jfif │ └── index.html ├── src │ ├── assets │ │ ├── featuring-arrow.png │ │ ├── profile-placeholder.png │ │ └── basketIcon.svg │ ├── styles │ │ ├── animations.scss │ │ ├── relatedProducts.scss │ │ ├── bestsellers.scss │ │ ├── index.scss │ │ ├── supply.scss │ │ ├── navigation.scss │ │ ├── header.scss │ │ ├── featuring.scss │ │ ├── headerSecondary.scss │ │ ├── home.scss │ │ ├── basket.scss │ │ └── Component.styles.js │ ├── http-common.js │ ├── components │ │ ├── products │ │ │ ├── Books.tsx │ │ │ ├── Films.tsx │ │ │ └── Songs.tsx │ │ ├── Footer.jsx │ │ ├── user │ │ │ ├── Logout.jsx │ │ │ ├── Join.jsx │ │ │ ├── profile.scss │ │ │ ├── Signin.jsx │ │ │ ├── Profile.jsx │ │ │ └── Register.jsx │ │ ├── home │ │ │ ├── Home.jsx │ │ │ ├── Supply.jsx │ │ │ ├── Bestsellers.jsx │ │ │ └── Featuring.jsx │ │ ├── index.js │ │ ├── storages │ │ │ ├── storages.scss │ │ │ └── Storages.tsx │ │ ├── Wishlist.jsx │ │ ├── templates │ │ │ ├── BasketButton.jsx │ │ │ ├── TemplatePage.jsx │ │ │ ├── WishlistButton.jsx │ │ │ └── TemplateProducts.jsx │ │ ├── Header.jsx │ │ ├── HeaderSecondary.jsx │ │ ├── Navigation.jsx │ │ ├── Basket.jsx │ │ └── ProductPage.jsx │ ├── redux │ │ ├── reducers │ │ │ ├── productReducer.js │ │ │ ├── index.js │ │ │ ├── userReducer.js │ │ │ ├── basketReducer.js │ │ │ └── wishlistReducer.js │ │ ├── store.js │ │ ├── actions │ │ │ ├── productActions.js │ │ │ ├── userActions.js │ │ │ ├── basketActions.js │ │ │ └── wishlistActions.js │ │ └── constants │ │ │ └── action-types.js │ ├── model │ │ └── user.ts │ ├── lib │ │ ├── localStorage.js │ │ └── helper.js │ ├── api │ │ └── userAPI.js │ ├── index.js │ └── App.js ├── .gitignore ├── package.json └── README.md ├── documentation ├── screenshots │ ├── Basket.png │ ├── Home.png │ ├── DetailedProduct.png │ └── README.md └── diagrams │ ├── table_view.png │ ├── entity_relationship.png │ └── entity_relationship.uml ├── server ├── database │ ├── inserts │ │ ├── address-inserts.sql │ │ ├── storage-inserts.sql │ │ ├── generate_songs_insert.py │ │ ├── generate_book_insert.py │ │ └── book-insert.sql │ ├── raw_json │ │ ├── wishlist.json │ │ └── users.json │ └── tables │ │ ├── create_users.sql │ │ └── create_tables.sql ├── src │ ├── lib │ │ ├── logger.js │ │ ├── daoHelper.js │ │ ├── fileHandler.js │ │ ├── scraper_books.py │ │ └── scraper_songs.py │ ├── dao │ │ ├── example.js │ │ └── main_dao.js │ └── routes │ │ └── router.js ├── config │ ├── db.js │ └── connection.js ├── package.json ├── app.js └── test │ └── fileHandler.test.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /server/.env 2 | -------------------------------------------------------------------------------- /client/public/Avatars/Wolf.jfif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeventeWolf/Bookshop/HEAD/client/public/Avatars/Wolf.jfif -------------------------------------------------------------------------------- /client/public/Avatars/Kredam.jfif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeventeWolf/Bookshop/HEAD/client/public/Avatars/Kredam.jfif -------------------------------------------------------------------------------- /client/public/Avatars/Selim.jfif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeventeWolf/Bookshop/HEAD/client/public/Avatars/Selim.jfif -------------------------------------------------------------------------------- /documentation/screenshots/Basket.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeventeWolf/Bookshop/HEAD/documentation/screenshots/Basket.png -------------------------------------------------------------------------------- /documentation/screenshots/Home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeventeWolf/Bookshop/HEAD/documentation/screenshots/Home.png -------------------------------------------------------------------------------- /client/src/assets/featuring-arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeventeWolf/Bookshop/HEAD/client/src/assets/featuring-arrow.png -------------------------------------------------------------------------------- /documentation/diagrams/table_view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeventeWolf/Bookshop/HEAD/documentation/diagrams/table_view.png -------------------------------------------------------------------------------- /client/src/assets/profile-placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeventeWolf/Bookshop/HEAD/client/src/assets/profile-placeholder.png -------------------------------------------------------------------------------- /documentation/diagrams/entity_relationship.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeventeWolf/Bookshop/HEAD/documentation/diagrams/entity_relationship.png -------------------------------------------------------------------------------- /documentation/screenshots/DetailedProduct.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeventeWolf/Bookshop/HEAD/documentation/screenshots/DetailedProduct.png -------------------------------------------------------------------------------- /server/database/inserts/address-inserts.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO wolf.address (city, zipcode, street, house_number) values ('Szeged', 6730, 'Kossuth Lajos', 1) -------------------------------------------------------------------------------- /client/src/styles/animations.scss: -------------------------------------------------------------------------------- 1 | .appear { 2 | animation-name: appear; 3 | animation-duration: 1s; 4 | } 5 | 6 | @keyframes appear { 7 | from {opacity: 0;} 8 | to {opacity: 1;} 9 | } 10 | -------------------------------------------------------------------------------- /client/src/http-common.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | export default axios.create({ 3 | baseURL: "http://localhost:3001/api", 4 | headers: { 5 | "Content-type": "application/json" 6 | } 7 | }); -------------------------------------------------------------------------------- /server/src/lib/logger.js: -------------------------------------------------------------------------------- 1 | function log (s) { 2 | const date = new Date().toISOString() 3 | .replace(/T/, ' ') 4 | .replace(/\..+/, '') ; 5 | 6 | console.log(`${date} | ${s}`) 7 | } 8 | 9 | module.exports = {log} 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /server/config/db.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | 3 | module.exports = { 4 | user : process.env.NODE_ORACLEDB_USERNAME || 'wolf', 5 | password : process.env.NODE_ORACLEDB_PASSWORD || 'yolo', 6 | connectString : process.env.NODE_ORACLEDB_CONNECTIONSTRING || 'localhost/XE', 7 | }; -------------------------------------------------------------------------------- /client/src/components/products/Books.tsx: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import React from 'react'; 3 | import TemplatePage from '../templates/TemplatePage' 4 | 5 | const Books = () => { 6 | return ( 7 | 8 | ); 9 | }; 10 | 11 | export default Books; -------------------------------------------------------------------------------- /client/src/components/products/Films.tsx: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import React from 'react'; 3 | import TemplatePage from '../templates/TemplatePage' 4 | 5 | const Films = () => { 6 | return ( 7 | 8 | ); 9 | }; 10 | 11 | export default Films; -------------------------------------------------------------------------------- /client/src/components/products/Songs.tsx: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import React from 'react'; 3 | import TemplatePage from '../templates/TemplatePage' 4 | 5 | const Songs = () => { 6 | return ( 7 | 8 | ); 9 | }; 10 | 11 | export default Songs; -------------------------------------------------------------------------------- /server/database/raw_json/wishlist.json: -------------------------------------------------------------------------------- 1 | { 2 | "wolf": [ 3 | "Clean Code", 4 | "Clean Architecture", 5 | "Refactoring", 6 | "Clean Craftsmanship" 7 | ], 8 | "edem46": [ 9 | "UML Distilled" 10 | ], 11 | "selim": [ 12 | "John Fowler", 13 | "Refaktoryzacja" 14 | ] 15 | } -------------------------------------------------------------------------------- /documentation/screenshots/README.md: -------------------------------------------------------------------------------- 1 | # Insights 2 | 3 | ### Home 4 | 5 | 6 | 7 | ### Basket 8 | 9 | 10 | 11 | ### Detailed Product 12 | 13 | 14 | 15 | ### Register & Login 16 | 17 | TODO 18 | 19 | ### Profile 20 | 21 | TODO -------------------------------------------------------------------------------- /client/src/styles/relatedProducts.scss: -------------------------------------------------------------------------------- 1 | .related-wrap { 2 | background-color: white; 3 | 4 | box-shadow: 0 1px 3px rgb(0 0 0 / 20%); 5 | } 6 | 7 | .related-products-container { 8 | margin-top: 10px; 9 | display: flex; 10 | width: 100%; 11 | 12 | padding: 5px 0; 13 | 14 | justify-content: space-between; 15 | } -------------------------------------------------------------------------------- /client/src/styles/bestsellers.scss: -------------------------------------------------------------------------------- 1 | .bestsellers-wrap { 2 | grid-area: bestsellers; 3 | 4 | position: relative; 5 | } 6 | 7 | .bestsellers-product-container { 8 | height: 100%; 9 | display: flex; 10 | flex-direction: column; 11 | align-items: center; 12 | } 13 | 14 | .bestsellers-product-wrap { 15 | 16 | } 17 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /client/src/redux/reducers/productReducer.js: -------------------------------------------------------------------------------- 1 | import { ProductActionTypes } from "../constants/action-types"; 2 | 3 | export const productReducer = (state = {}, {type, payload}) => { 4 | switch (type) { 5 | case ProductActionTypes.SELECTED_PRODUCT: 6 | return payload; 7 | case ProductActionTypes.REMOVE_SELECTED_PRODUCT: 8 | return {}; 9 | default: 10 | return state; 11 | } 12 | } -------------------------------------------------------------------------------- /client/src/components/Footer.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | function Footer() { 4 | return ( 5 |
6 |
7 |
8 |

9 | Follow Us 10 |

11 |
12 |
13 |
14 | ); 15 | } 16 | 17 | export default Footer; 18 | -------------------------------------------------------------------------------- /server/database/inserts/storage-inserts.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO WOLF.STORAGE (ID, NAME, CAPACITY, CITY) 2 | VALUES (1, 'Szeged Ujszeged', 4000, 'Szeged'); 3 | 4 | INSERT INTO WOLF.STORAGE (ID, NAME, CAPACITY, CITY) 5 | VALUES (2, 'Szeged Centrum', 300, 'Szeged'); 6 | 7 | INSERT INTO WOLF.STORAGE (ID, NAME, CAPACITY, CITY) 8 | VALUES (3, 'Budapest 1.', 20000, 'Budapest'); 9 | 10 | INSERT INTO WOLF.STORAGE (ID, NAME, CAPACITY, CITY) 11 | VALUES (4, 'Budapest 2.', 13000, 'Budapest'); 12 | 13 | -------------------------------------------------------------------------------- /client/src/redux/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from "redux"; 2 | import { userReducer } from "./userReducer"; 3 | import { basketReducer } from "./basketReducer"; 4 | import { productReducer } from "./productReducer"; 5 | import { wishlistReducer } from "./wishlistReducer"; 6 | 7 | const reducers = combineReducers({ 8 | user: userReducer, 9 | basket: basketReducer, 10 | product: productReducer, 11 | wishlist: wishlistReducer 12 | }); 13 | 14 | export default reducers; 15 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app.js", 6 | "scripts": { 7 | "start": "nodemon", 8 | "test": "jest" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "body-parser": "^1.19.0", 14 | "cors": "^2.8.5", 15 | "dotenv": "^16.0.0", 16 | "express": "^4.17.1", 17 | "jest": "^27.5.1", 18 | "nodemon": "^2.0.15", 19 | "oracledb": "5.1.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /client/src/model/user.ts: -------------------------------------------------------------------------------- 1 | export interface User { 2 | username: string, 3 | avatar: string, 4 | isLoggedIn: boolean, 5 | isMember: 0 | 1, 6 | IS_ADMIN: 0 | 1, 7 | boughtAmount: number, 8 | error: string, 9 | userLoggingIn: boolean 10 | } 11 | 12 | export const initialUser: User = { 13 | username: '', 14 | avatar: '', 15 | isLoggedIn: false, 16 | isMember: 0, 17 | IS_ADMIN: 0, 18 | boughtAmount: 0, 19 | error: '', 20 | userLoggingIn:false 21 | }; -------------------------------------------------------------------------------- /client/src/styles/index.scss: -------------------------------------------------------------------------------- 1 | .main-wrap { 2 | max-width: 1440px; 3 | 4 | margin: auto; 5 | padding: 10px 20px; 6 | } 7 | 8 | 9 | a { 10 | color: inherit; 11 | 12 | &:hover { 13 | color: inherit; 14 | } 15 | } 16 | 17 | .page-title { 18 | color: black; 19 | font-weight: bold; 20 | font-size: 2rem; 21 | font-family: Roboto, sans-serif; 22 | 23 | margin-bottom: 10px; 24 | 25 | letter-spacing: -1.5px; 26 | } 27 | 28 | .pwd-eye { 29 | filter: invert(0.5); 30 | } -------------------------------------------------------------------------------- /server/database/raw_json/users.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "username": "edem46", 4 | "password": "zizzen", 5 | "avatar": "Kredam.jfif", 6 | "isMember": true, 7 | "boughtAmount": 0 8 | }, 9 | { 10 | "username": "wolf", 11 | "password": "yolo", 12 | "avatar": "Wolf.jfif", 13 | "isMember": true, 14 | "boughtAmount": 0 15 | }, 16 | { 17 | "username": "selim", 18 | "password": "enhibam", 19 | "avatar": "Selim.jfif", 20 | "isMember": false, 21 | "boughtAmount": 0 22 | } 23 | ] -------------------------------------------------------------------------------- /server/config/connection.js: -------------------------------------------------------------------------------- 1 | process.env.ORA_SDTZ = 'UTC'; 2 | 3 | const fs = require('fs'); 4 | const oracledb = require('oracledb'); 5 | 6 | let libPath; // [Linux]: export LD_LIBRARY_PATH=/path/to/your/instantclient_19_14:$LD_LIBRARY_PATH 7 | 8 | if (process.platform === 'win32') { // Windows 9 | libPath = 'C:\\oracle\\instantclient_19_14'; 10 | } else if (process.platform === 'darwin') { // macOS 11 | libPath = process.env.HOME + '/Downloads/instantclient_19_8'; 12 | } 13 | if (libPath && fs.existsSync(libPath)) { 14 | oracledb.initOracleClient({libDir: libPath}); 15 | } -------------------------------------------------------------------------------- /client/src/lib/localStorage.js: -------------------------------------------------------------------------------- 1 | export const loadState = (key) => { 2 | try { 3 | const serializedState = localStorage.getItem(key); 4 | if (serializedState === null) { 5 | return undefined; 6 | } 7 | return JSON.parse(serializedState); 8 | } catch (err) { 9 | return undefined; 10 | } 11 | } 12 | 13 | export const saveState = (key, state) => { 14 | try { 15 | const serializedState = JSON.stringify(state); 16 | localStorage.setItem(key, serializedState); 17 | } catch (err) { 18 | // Ignore write errors. 19 | } 20 | } -------------------------------------------------------------------------------- /client/src/components/user/Logout.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {useDispatch} from "react-redux"; 3 | import {useEffect} from "react"; 4 | import {logout} from "../../redux/actions/userActions"; 5 | import {Text} from "@chakra-ui/react"; 6 | import {Navigate} from "react-router-dom"; 7 | 8 | export default function Logout() { 9 | const dispatch = useDispatch() 10 | 11 | useEffect(() => { 12 | dispatch(logout()) 13 | }, [dispatch]) 14 | 15 | return ( 16 | <> 17 | You are successfully longed out! 18 | 19 | 20 | ) 21 | } -------------------------------------------------------------------------------- /client/src/components/home/Home.jsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect} from "react"; 2 | import '../../styles/home.scss'; 3 | import Bestsellers from "./Bestsellers"; 4 | import Featuring from "./Featuring"; 5 | import Supply from "./Supply"; 6 | 7 | 8 | function Home() { 9 | useEffect(() => { 10 | document.title = "Bookshop" 11 | }, []); 12 | 13 | return ( 14 |
15 |
16 | 17 | 18 | 19 |
20 |
21 | ); 22 | } 23 | 24 | export default Home; 25 | -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | Bookshop 10 | 11 | 12 | 13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /server/app.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const cors = require('cors'); 3 | const path = require("path"); 4 | const bodyParser = require('body-parser') 5 | const app = express(); 6 | 7 | app.use(cors({origin: 'http://localhost:3000'})); 8 | 9 | const router = require('./src/routes/router.js'); 10 | 11 | const PORT = 3001 12 | 13 | app.use(express.static(path.join(__dirname, "./"))); 14 | app.use(bodyParser.json()); 15 | app.use(bodyParser.urlencoded({extended: true})) 16 | app.use(express.urlencoded({extended: false})); 17 | app.use(router) 18 | 19 | 20 | app.listen(PORT, () => { 21 | console.log("App listening at: http://localhost:3001/"); 22 | }); 23 | -------------------------------------------------------------------------------- /client/src/redux/store.js: -------------------------------------------------------------------------------- 1 | import {createStore, applyMiddleware, compose} from "redux"; 2 | import reducers from "./reducers/index"; 3 | import thunk from "redux-thunk"; 4 | import {loadState, saveState} from "../lib/localStorage" 5 | 6 | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; 7 | 8 | const preloadedStates = { 9 | product: loadState('product'), 10 | basket: loadState('basket'), 11 | user: loadState('user'), 12 | } 13 | 14 | const store = createStore( 15 | reducers, 16 | preloadedStates, 17 | composeEnhancers(applyMiddleware(thunk)) 18 | ); 19 | 20 | store.subscribe(() => { 21 | saveState('product', store.getState().product) 22 | saveState('basket', store.getState().basket) 23 | saveState('user', store.getState().user) 24 | }) 25 | 26 | export default store; 27 | -------------------------------------------------------------------------------- /client/src/components/index.js: -------------------------------------------------------------------------------- 1 | export { default as Navigation } from "./Navigation"; 2 | export { default as Header } from "./Header"; 3 | export { default as HeaderSecondary } from "./HeaderSecondary"; 4 | export { default as Footer } from "./Footer"; 5 | export { default as Join } from "./user/Join"; 6 | export { default as Logout } from "./user/Logout"; 7 | export { default as Profile } from "./user/Profile"; 8 | export { default as Home } from "./home/Home"; 9 | export { default as Basket } from "./Basket"; 10 | export { default as ProductPage } from "./ProductPage"; 11 | export { default as TemplatePage } from "./templates/TemplatePage"; 12 | export { default as Wishlist } from "./Wishlist"; 13 | export { default as Books } from "./products/Books"; 14 | export { default as Films } from "./products/Films"; 15 | export { default as Songs } from "./products/Songs"; -------------------------------------------------------------------------------- /client/src/redux/actions/productActions.js: -------------------------------------------------------------------------------- 1 | import {ProductActionTypes} from "../constants/action-types"; 2 | import Axios from 'axios'; 3 | 4 | export const fetchProduct = (productID, productTitle) => async (dispatch) => { 5 | Axios.get(`http://localhost:3001/api/product/${productID}/${productTitle}`) 6 | .then(response => { 7 | dispatch({type: ProductActionTypes.SELECTED_PRODUCT, payload: response.data}); 8 | }) 9 | .catch(response => { 10 | console.log(response) 11 | }); 12 | }; 13 | 14 | export const selectProduct = (product) => { 15 | return { 16 | type: ProductActionTypes.SELECTED_PRODUCT, 17 | payload: product 18 | } 19 | } 20 | 21 | export const removeSelectedProduct = () => { 22 | return { 23 | type: ProductActionTypes.REMOVE_SELECTED_PRODUCT, 24 | }; 25 | }; 26 | -------------------------------------------------------------------------------- /client/src/styles/supply.scss: -------------------------------------------------------------------------------- 1 | .supply-wrap { 2 | grid-area: supply; 3 | } 4 | 5 | .supply-container { 6 | display: flex; 7 | flex-wrap: wrap; 8 | align-items: center; 9 | justify-content: center; 10 | 11 | margin-top: 10px; 12 | } 13 | 14 | .supply-product { 15 | box-shadow: 0 1px 3px rgb(0 0 0 / 20%); 16 | 17 | width: 170px; 18 | 19 | padding: 10px; 20 | 21 | margin: 7px; 22 | 23 | .product-basket-btn { 24 | height: 36px; 25 | width: 100%; 26 | box-sizing: border-box; 27 | 28 | margin-top: 10px; 29 | } 30 | 31 | .product-title { 32 | margin-top: 10px; 33 | } 34 | 35 | .product-author { 36 | height: 30px; 37 | } 38 | 39 | .product-price { 40 | margin-top: 5px; 41 | } 42 | } 43 | 44 | .supply-product-image { 45 | max-width: 120px; 46 | max-height: 150px; 47 | height: 150px; 48 | object-fit: fill; 49 | } 50 | -------------------------------------------------------------------------------- /client/src/redux/constants/action-types.js: -------------------------------------------------------------------------------- 1 | export const BasketActionTypes = { 2 | ADD_PRODUCT: "ADD_PRODUCT", 3 | REMOVE_PRODUCT: "REMOVE_PRODUCT", 4 | CHECKOUT: 'CHECKOUT', 5 | INIT_PRODUCTS: 'INIT_PRODUCTS' 6 | } 7 | 8 | export const UserActionTypes = { 9 | LOGIN_LOADING: "LOGIN_LOADING", 10 | LOGIN_ERROR : "LOGIN_ERROR", 11 | LOGIN_SUCCESS:"LOGIN_SUCCESS", 12 | LOGOUT: "LOGOUT", 13 | REGISTER: "REGISTER", 14 | UPDATE_MEMBER: "UPDATE_MEMBER", 15 | SET_BOUGHT_AMOUNT: "SET_BOUGHT_AMOUNT" 16 | } 17 | 18 | export const ProductActionTypes = { 19 | SELECTED_PRODUCT: "SELECT_PRODUCT", 20 | REMOVE_SELECTED_PRODUCT: "REMOVE_SELECTED_PRODUCT" 21 | } 22 | 23 | export const WishlistActionTypes = { 24 | INIT_WISHLIST: "INIT_WISHLIST", 25 | CLEAR_WISHLIST: "CLEAR_WISHLIST_WISHLIST", 26 | ADD_PRODUCT_TO_WISHLIST: "ADD_PRODUCT_TO_WISHLIST", 27 | REMOVE_PRODUCT: "REMOVE_PRODUCT_WISHLIST" 28 | } -------------------------------------------------------------------------------- /client/src/lib/helper.js: -------------------------------------------------------------------------------- 1 | export function numberWithSpaces(x) { 2 | return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, " "); 3 | } 4 | 5 | export function shuffleArray(array) { 6 | for (var i = array.length - 1; i > 0; i--) { 7 | var j = Math.floor(Math.random() * (i + 1)); 8 | var temp = array[i]; 9 | array[i] = array[j]; 10 | array[j] = temp; 11 | } 12 | } 13 | 14 | 15 | export function numberOfProducts(basket) { 16 | let result = 0; 17 | 18 | basket.forEach(product => result += product.quantity); 19 | 20 | return result; 21 | } 22 | 23 | export function sumOfProducts(basket, isMember) { 24 | let sum = 0; 25 | 26 | basket.forEach(product => { 27 | if (isMember) { 28 | sum += product.quantity * Math.round(product.price * 0.9); 29 | } else { 30 | sum += product.quantity * product.price; 31 | } 32 | }) 33 | 34 | return sum; 35 | } -------------------------------------------------------------------------------- /client/src/components/user/Join.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Register } from './Register' 3 | import { Signin } from './Signin' 4 | import { Flex, Spacer, Box } from '@chakra-ui/react' 5 | 6 | function Join() { 7 | return ( 8 | // 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | ); 23 | } 24 | 25 | export default Join; 26 | -------------------------------------------------------------------------------- /client/src/components/storages/storages.scss: -------------------------------------------------------------------------------- 1 | .storages { 2 | h1 { 3 | font-size: 20pt; 4 | font-weight: bold; 5 | margin-bottom: 15px; 6 | } 7 | 8 | .storages-container { 9 | padding: 10px; 10 | background-color: white; 11 | box-shadow: 1px 1px 2px rgba(198, 174, 174, 0.72); 12 | 13 | gap: 20px; 14 | 15 | display: flex; 16 | flex-direction: column; 17 | 18 | .storage-wrap { 19 | padding: 10px; 20 | border: 1px solid rgba(128, 128, 128, 0.55); 21 | box-shadow: 1px 1px 2px rgba(198, 174, 174, 0.72); 22 | } 23 | } 24 | 25 | 26 | .product-info-wrap { 27 | margin-top: 10px; 28 | 29 | .title-wrap { 30 | display: flex; 31 | gap: 5px; 32 | align-items: center; 33 | 34 | h2 { font-weight: bold } 35 | 36 | .down-arrow { 37 | height: 20px; 38 | cursor: pointer; 39 | } 40 | } 41 | 42 | .product-container { 43 | 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /client/src/redux/actions/userActions.js: -------------------------------------------------------------------------------- 1 | import {UserActionTypes} from "../constants/action-types"; 2 | 3 | export const loginSuccess = (userData) => { 4 | return { 5 | type: UserActionTypes.LOGIN_SUCCESS, 6 | payload: userData 7 | } 8 | } 9 | 10 | export const loginLoading = () => { 11 | return { 12 | type: UserActionTypes.LOGIN_LOADING, 13 | } 14 | } 15 | 16 | export const loginError = (error) => { 17 | return { 18 | type: UserActionTypes.LOGIN_ERROR, 19 | payload: error 20 | } 21 | } 22 | 23 | export const logout = () => { 24 | return { 25 | type: UserActionTypes.LOGOUT, 26 | } 27 | } 28 | 29 | export const updateMember = () => { 30 | return { 31 | type: UserActionTypes.UPDATE_MEMBER, 32 | } 33 | } 34 | 35 | export const setBoughtAmount = (amount) => { 36 | return { 37 | type: UserActionTypes.SET_BOUGHT_AMOUNT, 38 | payload: amount, 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /client/src/components/home/Supply.jsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useState} from "react"; 2 | import '../../styles/supply.scss'; 3 | import {v4} from "uuid"; 4 | import Axios from "axios"; 5 | import {ProductM} from "../templates/TemplateProducts"; 6 | 7 | export default function Supply() { 8 | const [products, setProducts] = useState([]); 9 | 10 | useEffect(() => { 11 | Axios.get('http://localhost:3001/api/all-books') 12 | .then(response => { 13 | // shuffleArray(response.data); 14 | setProducts(response.data); 15 | }) 16 | .catch(response => { 17 | console.log(response) 18 | }); 19 | }, []) 20 | 21 | return ( 22 |
23 |

Supply

24 | 25 |
26 | {products.map(product => )} 27 |
28 |
29 | ) 30 | } -------------------------------------------------------------------------------- /client/src/styles/navigation.scss: -------------------------------------------------------------------------------- 1 | .nav-wrap { 2 | background: #403042; 3 | } 4 | 5 | .nav-primary-wrap { 6 | max-width: 1440px; 7 | margin: auto; 8 | padding: 0 20px; 9 | 10 | display: flex; 11 | justify-content: right; 12 | 13 | } 14 | 15 | .nav-home, .nav-join, .nav-watchlist, .nav-profile { 16 | color: rgba(255,255,255,0.8); 17 | font-size: 12pt; 18 | 19 | padding: 15px; 20 | 21 | margin-left: 15px; 22 | 23 | //border: 1px solid white; 24 | 25 | text-decoration: none; 26 | 27 | transition: color .3s; 28 | 29 | &:hover { 30 | color: white; 31 | 32 | filter: brightness(0) invert(1); 33 | } 34 | 35 | &:active { 36 | color: white; 37 | } 38 | } 39 | 40 | .nav-profile { 41 | &:hover { 42 | filter: brightness(130%); 43 | } 44 | } 45 | 46 | .nav-icon { 47 | display: inline; 48 | position: relative; 49 | bottom: 2px; 50 | width: 20px; 51 | margin-right: 10px; 52 | 53 | 54 | transition: all .3s; 55 | filter: brightness(0) invert(0.9); 56 | } -------------------------------------------------------------------------------- /client/src/components/Wishlist.jsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect} from "react"; 2 | 3 | import { 4 | Main, 5 | PageTitle 6 | } from "../styles/Component.styles"; 7 | import {useDispatch, useSelector} from "react-redux"; 8 | import {ProductWishlist} from "./templates/TemplateProducts"; 9 | import {fetchWishlistProducts} from "../redux/actions/wishlistActions"; 10 | 11 | 12 | export default function Wishlist( ) { 13 | const wishlistProducts = useSelector(state => state.wishlist) 14 | const user = useSelector(state => state.user) 15 | const dispatch = useDispatch() 16 | 17 | useEffect(() => { 18 | dispatch(fetchWishlistProducts(user.username)); 19 | }, [dispatch, user.username]) 20 | 21 | return ( 22 |
23 |
24 | Wishlist 25 | 26 | {wishlistProducts.map(product => )} 27 |
28 |
29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /client/src/components/templates/BasketButton.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {useDispatch} from "react-redux"; 3 | import {addProductToBasket} from "../../redux/actions/basketActions"; 4 | import styled from "styled-components"; 5 | 6 | export const Button = styled.button` 7 | width: ${props => props.w}; 8 | height: ${props => props.h}; 9 | 10 | border-radius: 5px; 11 | 12 | background-color: #0cceeb; 13 | color: white; 14 | font-size: 11pt; 15 | font-weight: bold; 16 | font-family: Roboto, sans-serif; 17 | 18 | transition: all .3s; 19 | 20 | &:hover { 21 | filter: brightness(108%); 22 | } 23 | `; 24 | 25 | export default function BasketButton ( {children, product, style} ) { 26 | const dispatch = useDispatch(); 27 | 28 | function handleAddProductToBasket() { 29 | dispatch(addProductToBasket(product)); 30 | } 31 | 32 | 33 | return ( 34 | 37 | ) 38 | } -------------------------------------------------------------------------------- /server/src/lib/daoHelper.js: -------------------------------------------------------------------------------- 1 | function getDate() { 2 | function pad2(n) { 3 | return (n < 10 ? '0' : '') + n; 4 | } 5 | 6 | const date = new Date(); 7 | const month = pad2(date.getMonth() + 1);//months (0-11) 8 | const day = pad2(date.getDate());//day (1-31) 9 | const year = date.getFullYear(); 10 | const hour = date.getHours(); 11 | const minute = date.getMinutes(); 12 | const sec = date.getSeconds(); 13 | 14 | return year + "/" + month + "/" + day + "T" + hour + ':' + minute + ':' + sec; 15 | } 16 | 17 | function convertKeysLowercase(obj) { 18 | let key, keys = Object.keys(obj); 19 | let n = keys.length; 20 | let result = {}; 21 | while (n--) { 22 | key = keys[n]; 23 | result[key.toLowerCase()] = obj[key]; 24 | } 25 | 26 | return result; 27 | } 28 | 29 | function formatRows(rows) { 30 | const result = []; 31 | 32 | rows.forEach(row => result.push(convertKeysLowercase(row))) 33 | 34 | return result; 35 | } 36 | 37 | 38 | module.exports = {getDate, formatRow: formatRows} -------------------------------------------------------------------------------- /client/src/redux/actions/basketActions.js: -------------------------------------------------------------------------------- 1 | import { BasketActionTypes } from "../constants/action-types"; 2 | import Axios from "axios"; 3 | 4 | export const initBasketProducts = (products) => { 5 | return { 6 | type: BasketActionTypes.INIT_PRODUCTS, 7 | payload: products, 8 | } 9 | } 10 | 11 | export const addProductToBasket = (product) => { 12 | return { 13 | type: BasketActionTypes.ADD_PRODUCT, 14 | payload: product, 15 | } 16 | } 17 | 18 | export const removeProductFromBasket = (product) => { 19 | return { 20 | type: BasketActionTypes.REMOVE_PRODUCT, 21 | payload: product 22 | } 23 | } 24 | 25 | 26 | export const handleCheckoutAction = (username, products) => async (dispatch) => { 27 | Axios.post(`http://localhost:3001/api/checkout/`, { 28 | username, 29 | products 30 | }).then(response => { 31 | dispatch(checkout()) 32 | }).catch(response => { 33 | console.log(response) 34 | }); 35 | } 36 | 37 | 38 | export const checkout = () => { 39 | return { 40 | type: BasketActionTypes.CHECKOUT, 41 | } 42 | } -------------------------------------------------------------------------------- /client/src/components/home/Bestsellers.jsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useState} from "react"; 2 | import '../../styles/bestsellers.scss'; 3 | import {v4} from "uuid"; 4 | import Axios from "axios"; 5 | import {ProductM} from "../templates/TemplateProducts"; 6 | 7 | export default function Bestsellers() { 8 | const [products, setProducts] = useState([]); 9 | 10 | useEffect(() => { 11 | Axios.get('http://localhost:3001/api/bestsellers') 12 | .then(response => { 13 | // shuffleArray(response.data); 14 | setProducts(response.data.splice(0, 5)); 15 | }) 16 | .catch(response => { 17 | console.log(response) 18 | }); 19 | }, []) 20 | 21 | return ( 22 |
23 |

Bestsellers

24 | 25 |
26 |
27 | {products.map(product => )} 28 |
29 |
30 | 31 |
32 | ) 33 | }; 34 | 35 | -------------------------------------------------------------------------------- /client/src/components/templates/TemplatePage.jsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useLayoutEffect, useState} from "react"; 2 | 3 | import { 4 | Main, 5 | PageTitle, 6 | } from "../../styles/Component.styles"; 7 | import Axios from "axios"; 8 | import {ProductTemplate} from "./TemplateProducts"; 9 | import v4 from "uuid/v4"; 10 | 11 | 12 | export default function TemplatePage( {name, api} ) { 13 | const [products, setProducts] = useState([]); 14 | 15 | useLayoutEffect(() => { 16 | fetchProducts(); 17 | }, []) 18 | 19 | function fetchProducts() { 20 | Axios.get(`http://localhost:3001/api/${api}`) 21 | .then(response => { 22 | setProducts(response.data); 23 | }) 24 | .catch(response => { 25 | console.log(response) 26 | }) 27 | } 28 | 29 | 30 | return ( 31 |
32 |
33 | {name} 34 | 35 | {/* TODO: BUG product.id is not unique */} 36 | {products.map(product => )} 37 |
38 |
39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /client/src/api/userAPI.js: -------------------------------------------------------------------------------- 1 | import apiClient from '../http-common' 2 | import {loginError, loginLoading, loginSuccess} from "../redux/actions/userActions"; 3 | 4 | export const signin = (userData) => async dispatch => { 5 | await apiClient.post("/signin", {userData}).then(res => { 6 | dispatch(loginLoading()) 7 | if (res.data.isAuthenticated){ 8 | dispatch(loginSuccess(res.data.user)) 9 | return true 10 | } 11 | }) 12 | .catch(error => dispatch(loginError(error.message))); 13 | 14 | } 15 | 16 | export async function register(emailRef, usernameRef, passwordRef, callback, dispatch) { 17 | const response = await apiClient.post('/registration', { 18 | email: emailRef, 19 | username: usernameRef, 20 | password: passwordRef, 21 | }).catch( 22 | err => { 23 | console.log(err.response.data) 24 | callback(() => err.response.data) 25 | } 26 | ) 27 | if(response){ 28 | dispatch(loginSuccess({username:usernameRef, avatar:'', isMember:true})) 29 | } 30 | } 31 | 32 | export async function getUserData(username){ 33 | return await apiClient.post('/user', {username}) 34 | } 35 | 36 | export default {signin, register} -------------------------------------------------------------------------------- /client/src/redux/reducers/userReducer.js: -------------------------------------------------------------------------------- 1 | import {UserActionTypes} from "../constants/action-types"; 2 | 3 | const initialUser = { 4 | username: undefined, 5 | avatar: undefined, 6 | isLoggedIn: false, 7 | isMember: false, 8 | isAdmin: false, 9 | boughtAmount: 0, 10 | error: null, 11 | userLoggingIn:false 12 | }; 13 | 14 | export const userReducer = (user = initialUser, {type, payload}) => { 15 | switch (type) { 16 | case UserActionTypes.LOGIN_SUCCESS: 17 | return {...payload, isLoggedIn: true}; 18 | 19 | case UserActionTypes.LOGIN_ERROR: 20 | return {error:payload, isLoggedIn: false} 21 | 22 | case UserActionTypes.LOGIN_LOADING: 23 | return {...payload, userLoggingIn: true} 24 | 25 | case UserActionTypes.LOGOUT: 26 | return initialUser; 27 | 28 | case UserActionTypes.UPDATE_MEMBER: 29 | if (user.isMember) return user; 30 | 31 | if (user.boughtAmount >= 5) { 32 | return {...user, isMember: true}; 33 | } 34 | 35 | return {...user, isMember: false}; 36 | 37 | case UserActionTypes.SET_BOUGHT_AMOUNT: 38 | return {...user, boughtAmount: payload}; 39 | 40 | 41 | default: 42 | return user; 43 | } 44 | }; -------------------------------------------------------------------------------- /client/src/redux/reducers/basketReducer.js: -------------------------------------------------------------------------------- 1 | import { BasketActionTypes } from "../constants/action-types"; 2 | 3 | export const basketReducer = (basket = [], {type, payload}) => { 4 | let basketCopy = [...basket]; 5 | 6 | switch (type) { 7 | case BasketActionTypes.INIT_PRODUCTS: 8 | return payload; 9 | 10 | case BasketActionTypes.ADD_PRODUCT: 11 | const product = payload; 12 | 13 | // If already has this product, increase quantity 14 | for (const basketProduct of basket) { 15 | if (basketProduct.name === product.name) { 16 | basketProduct.quantity++; 17 | return basketCopy; 18 | } 19 | } 20 | 21 | // Else add new product 22 | return [...basketCopy, product]; 23 | 24 | case BasketActionTypes.REMOVE_PRODUCT: 25 | for (const basketProduct of basket) { 26 | if (basketProduct.name === payload.name) { 27 | basketProduct.quantity--; 28 | 29 | break; 30 | } 31 | } 32 | 33 | return (basketCopy.filter(product => product.quantity !== 0)); 34 | 35 | case BasketActionTypes.CHECKOUT: 36 | return []; 37 | 38 | default: 39 | return basket; 40 | } 41 | } -------------------------------------------------------------------------------- /client/src/redux/reducers/wishlistReducer.js: -------------------------------------------------------------------------------- 1 | import { WishlistActionTypes } from "../constants/action-types"; 2 | 3 | export const wishlistReducer = (wishlist = [], {type, payload}) => { 4 | let wishlistCopy = [...wishlist]; 5 | 6 | switch (type) { 7 | case WishlistActionTypes.INIT_WISHLIST: 8 | return payload; 9 | 10 | case WishlistActionTypes.ADD_PRODUCT_TO_WISHLIST: 11 | const product = payload; 12 | 13 | // If already has this product, do not add 14 | for (const wishlistProduct of wishlistCopy) { 15 | if (wishlistProduct.name === product.name) { 16 | return wishlist; 17 | } 18 | } 19 | 20 | // Add new product 21 | return [...wishlistCopy, product]; 22 | 23 | case WishlistActionTypes.REMOVE_PRODUCT: 24 | for (const wishlistProduct of wishlistCopy) { 25 | if (wishlistProduct.name === payload.name) { 26 | wishlistProduct.quantity--; 27 | 28 | break; 29 | } 30 | } 31 | 32 | return (wishlistCopy.filter(product => product.quantity !== 0)); 33 | 34 | case WishlistActionTypes.CLEAR_WISHLIST: 35 | return []; 36 | 37 | default: 38 | return wishlist; 39 | } 40 | } -------------------------------------------------------------------------------- /client/src/assets/basketIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /client/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import {ChakraProvider} from '@chakra-ui/react' 4 | import {Provider} from 'react-redux'; 5 | import {QueryClient, QueryClientProvider} from "react-query"; 6 | import {ReactQueryDevtools} from "react-query/devtools"; 7 | import AlertTemplate from 'react-alert-template-basic' 8 | import {transitions, positions, Provider as AlertProvider} from 'react-alert' 9 | import store from './redux/store' 10 | import App from "./App"; 11 | 12 | const alertOptions = { 13 | position: positions.BOTTOM_RIGHT, 14 | timeout: 5000, 15 | offset: '5px', 16 | transition: transitions.FADE 17 | } 18 | 19 | const queryClient = new QueryClient({ 20 | defaultOptions:{ 21 | queries:{ 22 | // refetchOnMount: false, 23 | // refetchOnReconnect: false, 24 | // refetchOnWindowFocus: false, 25 | // retry:false 26 | } 27 | } 28 | }) 29 | 30 | ReactDOM.render( 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | , 41 | 42 | 43 | document.getElementById("root") 44 | ); 45 | -------------------------------------------------------------------------------- /client/src/components/user/profile.scss: -------------------------------------------------------------------------------- 1 | .profile { 2 | display: flex; 3 | flex-direction: column; 4 | gap: 20px; 5 | background-color: white; 6 | box-shadow: 1px 1px 2px rgba(198, 174, 174, 0.72); 7 | padding: 20px; 8 | 9 | h1, h2 { 10 | font-weight: bold; 11 | } 12 | 13 | h1 { 14 | font-size: 20pt; 15 | } 16 | 17 | h2 { 18 | margin-bottom: 5px; 19 | } 20 | 21 | .client-data-container { 22 | display: flex; 23 | gap: 20px; 24 | 25 | .profile-picture { 26 | border-radius: 50% 27 | } 28 | 29 | .client-info-container { 30 | display: flex; 31 | flex-direction: column; 32 | //justify-content: space-between; 33 | //gap: 10px; 34 | } 35 | } 36 | 37 | .address-container { 38 | 39 | } 40 | 41 | .credit-card-container { 42 | 43 | } 44 | 45 | 46 | } 47 | 48 | 49 | .purchases-info-container { 50 | .purchases-container { 51 | display: flex; 52 | flex-direction: column; 53 | gap: 10px; 54 | 55 | .purchases-wrap { 56 | padding-bottom: 10px; 57 | border: 1px solid rgba(128, 128, 128, 0.47); 58 | box-shadow: 1px 1px 2px rgba(198, 174, 174, 0.72); 59 | 60 | .products-container { 61 | table tr td:first-child { 62 | width: 700px !important; 63 | } 64 | } 65 | 66 | .date { 67 | font-size: 16px; 68 | font-style: italic; 69 | margin-left: 10px; 70 | } 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /server/src/lib/fileHandler.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | const books = JSON.parse(fs.readFileSync('./database/raw_json/books.json')); 4 | const users = JSON.parse(fs.readFileSync('./database/raw_json/users.json')); 5 | const wishlist = JSON.parse(fs.readFileSync('./database/raw_json/wishlist.json')); 6 | 7 | 8 | function getAllBooks(){ 9 | return books; 10 | } 11 | 12 | function getUser(username, password){ 13 | return users.find(user => user.username === username && user.password === password) 14 | } 15 | 16 | function getWishlistProducts(username) { 17 | const products = []; 18 | 19 | try { 20 | const userWishlistTitles = wishlist[username]; 21 | 22 | for (const title of userWishlistTitles) { 23 | products.push(getProductByTitle(title)) 24 | } 25 | } catch (err) { 26 | // Handle errors 27 | } 28 | 29 | return products; 30 | } 31 | 32 | function getProductByTitle(title) { 33 | return books.find(book => book.title === title); 34 | } 35 | 36 | function addProductToWishlist(username, product) { 37 | if (wishlist[username].includes(product.title)) return; 38 | 39 | wishlist[username].push(product.title); 40 | } 41 | 42 | function removeProductFromWishlist(username, product) { 43 | wishlist[username] = wishlist[username].filter(productTitle => productTitle !== product.title); 44 | } 45 | 46 | 47 | module.exports = {getAllBooks, getUser, getProductByTitle, 48 | getWishlistProducts, addProductToWishlist, removeProductFromWishlist} -------------------------------------------------------------------------------- /server/database/inserts/generate_songs_insert.py: -------------------------------------------------------------------------------- 1 | import json 2 | import random 3 | 4 | songs_raw_path = '../raw_json/songs.json' 5 | songs_insert_path = 'songs-insert.sql' 6 | 7 | 8 | def insert_to_songs(id, book): 9 | template = "INSERT INTO WOLF.SONG (ID, LENGTH, PRODUCER) " \ 10 | "VALUES ({id}, '{length}', '{producer}');" 11 | 12 | result = template.format( 13 | id=id, 14 | length=book['length'], 15 | producer=book['producer'], 16 | ) 17 | 18 | return result 19 | 20 | 21 | def insert_to_products(id, song): 22 | template = "INSERT INTO WOLF.PRODUCT (ID, PRICE, NAME, GENRE, RELEASE, IMAGEURL, DESCRIPTION) " \ 23 | "VALUES ({id}, {price}, '{name}', '{genre}', '{date}', '{imageURL}', '{description}');" 24 | 25 | result = template.format( 26 | id=id, 27 | price=song['price'], 28 | name=song['title'], 29 | date='', 30 | genre=song['genre'], 31 | imageURL=song['imageUrl'], 32 | description=song['description'], 33 | ) 34 | 35 | return result 36 | 37 | 38 | def get_songs(): 39 | with open(songs_raw_path, 'r') as books_json: 40 | return json.load(books_json) 41 | 42 | 43 | def generate_book_inserts(): 44 | songs = get_songs() 45 | 46 | id = 31 47 | 48 | with open(songs_insert_path, 'w') as songs_sql: 49 | for song in songs: 50 | songs_sql.write(insert_to_products(id, song) + '\n') 51 | songs_sql.write(insert_to_songs(id, song) + '\n\n') 52 | 53 | print(insert_to_products(id, song)) 54 | print(insert_to_songs(id, song)) 55 | id += 1 56 | print() 57 | 58 | 59 | if __name__ == '__main__': 60 | generate_book_inserts() 61 | -------------------------------------------------------------------------------- /client/src/styles/header.scss: -------------------------------------------------------------------------------- 1 | .header-wrap { 2 | color: #fff; 3 | background: #4c394e; 4 | position: relative; 5 | z-index: 10; 6 | 7 | height: 80px; 8 | } 9 | 10 | .header { 11 | max-width: 1440px; 12 | } 13 | .header, .header-wrap { 14 | margin-right: auto; 15 | margin-left: auto; 16 | padding-left: 1rem; 17 | padding-right: 1rem; 18 | } 19 | 20 | .primary-wrap { 21 | box-sizing: border-box; 22 | padding: 10px; 23 | 24 | display: flex; 25 | } 26 | .primary-wrap .brand-link { 27 | display: block; 28 | width: 154px; 29 | max-width: 100%; 30 | } 31 | 32 | .search-wrap { 33 | margin: auto; 34 | text-align: right; 35 | width: 100%; 36 | position: relative; 37 | float: right; 38 | } 39 | 40 | .search-icon { 41 | display: inline; 42 | 43 | width: 23px; 44 | position: relative; 45 | left: 35px; 46 | bottom: 2px; 47 | } 48 | 49 | .text-input { 50 | width: 60%; 51 | height: 44px; 52 | 53 | padding-left: 40px; 54 | } 55 | 56 | .header-search-btn, .header-advanced-search-btn { 57 | width: 150px; 58 | height: 45px; 59 | 60 | box-sizing: border-box; 61 | 62 | margin-left: 10px; 63 | font-size: 11pt; 64 | } 65 | 66 | .header-search-btn { 67 | border-radius: 5px; 68 | border: none; 69 | 70 | background-color: rgb(16, 187, 213); 71 | transition: background .3s; 72 | 73 | &:hover { 74 | background-color: #29D5EF; 75 | } 76 | } 77 | 78 | .header-advanced-search-btn { 79 | border: 1px solid rgba(255, 255, 255, 0.7); 80 | background-color: inherit; 81 | color: rgba(255, 255, 255, 0.7); 82 | 83 | transition: all .3s; 84 | 85 | &:hover { 86 | border: 1px solid white; 87 | color: white; 88 | } 89 | } 90 | 91 | .text { 92 | color: white; 93 | } -------------------------------------------------------------------------------- /client/src/App.js: -------------------------------------------------------------------------------- 1 | import {BrowserRouter as Router, Route, Routes} from "react-router-dom"; 2 | import { 3 | Basket, 4 | Header, 5 | HeaderSecondary, 6 | Home, 7 | Logout, 8 | Join, 9 | Navigation, 10 | ProductPage, 11 | Profile, 12 | TemplatePage, 13 | Wishlist 14 | } from "./components"; 15 | import React from "react"; 16 | 17 | import "./styles/index.scss"; 18 | import Books from "./components/products/Books"; 19 | import Films from "./components/products/Films"; 20 | import Songs from "./components/products/Songs"; 21 | import Storages from "./components/storages/Storages"; 22 | 23 | 24 | export default function App(){ 25 | document.body.style.backgroundColor = '#F0F0F0'; 26 | 27 | return ( 28 | 29 | 30 |
31 | 32 | 33 | 34 | }/> 35 | }/> 36 | }/> 37 | }/> 38 | }/> 39 | }/> 40 | 41 | {/* TemplatePages */} 42 | }/> 43 | }/> 44 | }/> 45 | }/> 46 | }/> 47 | }/> 48 | 49 | 50 | {/*