diff --git a/README.md b/README.md index 761e747..ab18b5a 100644 Binary files a/README.md and b/README.md differ diff --git a/client/public/components/basket-popout.mjs b/client/public/components/basket-popout.mjs index fabadf0..3c77269 100644 --- a/client/public/components/basket-popout.mjs +++ b/client/public/components/basket-popout.mjs @@ -61,8 +61,8 @@ export function RemoveProductFromBasket(product, type, amount, brickModifier = ' product += '~' + brickModifier; } - if (basket.items[product] > amount) { - basket.items[product] -= amount; + if (basket.items[product].quantity > amount) { + basket.items[product].quantity -= amount; } else { delete basket.items[product]; } @@ -76,6 +76,39 @@ export function RemoveProductFromBasket(product, type, amount, brickModifier = ' } } +export function SetProductInBasket(product, type, amount, brickModifier = 'none') { + if (localStorage.getItem('basket') === null || !localStorage.getItem('basket')) { + return; + } + const basket = JSON.parse(localStorage.getItem('basket')); + + if (type === 'brick') { + product += '~' + brickModifier; + } + + if (amount === 0) { + delete basket.items[product]; + } else { + if (basket.items[product]) { + basket.total -= basket.items[product].quantity; + basket.items[product].quantity = amount; + } else { + basket.items[product] = { + quantity: amount, + type, + }; + } + } + + basket.total += amount; + + localStorage.setItem('basket', JSON.stringify(basket)); + + if (basketCallback) { + basketCallback(); + } +} + export function GetBasketTotal() { if (localStorage.getItem('basket') === null || !localStorage.getItem('basket')) { return 0; @@ -86,8 +119,25 @@ export function GetBasketTotal() { return basket.total; } -export function GetBasketTotalPrice() { - +export async function GetBasketTotalPrice() { + if (localStorage.getItem('basket') === null || !localStorage.getItem('basket')) { + return 0; + } + + const basket = JSON.parse(localStorage.getItem('basket')); + + const res = await fetch('/api/basket/price', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(basket), + }).then(res => res.json()); + + if (res.error) { + return 0; + } + return res.data.subtotal; } class BasketPopout extends Component { @@ -103,19 +153,11 @@ class BasketPopout extends Component { if (basket) { try { const basketJSON = JSON.parse(basket); - - const res = await fetch('/api/basket/price', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(basketJSON), - }).then(res => res.json()); - + const subtotal = await GetBasketTotalPrice(); this.setState({ items: basketJSON.items, total: basketJSON.total, - subtotal: res.data.subtotal, + subtotal, }); } catch (e) { console.log(e); diff --git a/client/public/components/basket.mjs b/client/public/components/basket.mjs index 04119f0..4005561 100644 --- a/client/public/components/basket.mjs +++ b/client/public/components/basket.mjs @@ -1,4 +1,4 @@ -import { AddProductToBasket, GetBasketTotal, RemoveProductFromBasket } from './basket-popout.mjs'; +import { AddProductToBasket, SetProductInBasket, RemoveProductFromBasket, GetBasketTotal, GetBasketTotalPrice } from './basket-popout.mjs'; import { RegisterComponent, Component } from './components.mjs'; class Basket extends Component { @@ -32,7 +32,7 @@ class Basket extends Component { Your Basket
- Total: {this.state.total} items + Total: {this.state.total} items
@@ -60,6 +60,13 @@ class Basket extends Component { `; }).join('')}
+ `, style: ` @@ -137,7 +144,16 @@ class Basket extends Component { }; } + async UpdateSubtotal() { + const subtotal = await GetBasketTotalPrice(); + const basketSubtotal = this.root.querySelector('.basket-subtotal'); + if (basketSubtotal) { + basketSubtotal.innerText = parseFloat(subtotal).toFixed(2); + } + } + OnRender() { + this.UpdateSubtotal(); this.root.querySelectorAll('.basket-item-listing').forEach((listing) => { // listen to mutations on the attribute stock because the stock is updated once the // super compact listing is loaded and the stock is updated @@ -158,7 +174,6 @@ class Basket extends Component { ...this.state, }, false); } - // update the stock number const stockNumber = mutation.target.parentElement.querySelector('.stock-number'); stockNumber.innerText = stock; @@ -173,6 +188,7 @@ class Basket extends Component { }); }); + // set up each button to update the quantity and remove if it is zero this.root.querySelectorAll('.product-quantity-button').forEach((button) => { button.addEventListener('click', (event) => { @@ -197,7 +213,16 @@ class Basket extends Component { } if (item.quantity === 0) { RemoveProductFromBasket(id, item.type, item.quantity, modifier); + this.UpdateSubtotal(); delete this.state.items[compositeId]; + + return this.setState({ + ...this.state, + total: GetBasketTotal(), + items: { + ...this.state.items, + }, + }); } } else if (event.target.classList.contains('increase-quantity')) { if (item.quantity < item.stock) { @@ -206,7 +231,16 @@ class Basket extends Component { } } else if (event.target.classList.contains('remove-quantity')) { RemoveProductFromBasket(id, item.type, item.quantity, modifier); + this.UpdateSubtotal(); delete this.state.items[compositeId]; + + return this.setState({ + ...this.state, + total: GetBasketTotal(), + items: { + ...this.state.items, + }, + }); } // update the total @@ -217,7 +251,72 @@ class Basket extends Component { ...this.state.items, [compositeId]: item, }, - }); + }, false); + + // update the total so it doesn't need to be rendered again + const total = this.root.querySelector('.basket-total'); + total.innerText = this.state.total; + // and the same on the item + const itemTotal = clickedItem.querySelector('.quantity-input'); + itemTotal.value = parseInt(item.quantity); + this.UpdateSubtotal(); + }); + }); + + + // do the same but with the input field + this.root.querySelectorAll('.quantity-input').forEach((input) => { + input.addEventListener('change', (event) => { + let clickedItem = event.target.parentElement; + let listing = clickedItem.querySelector('.basket-item-listing'); + if (!listing) { + clickedItem = clickedItem.parentElement; + listing = clickedItem.querySelector('.basket-item-listing'); + } + + const id = listing.getAttribute('id'); + const modifier = listing.getAttribute('modifier'); + const compositeId = id + (modifier ? `~${modifier}` : ''); + const item = this.state.items[compositeId]; + + // update the quantity + if (event.target.value < 0) { + event.target.value = 0; + } + if (event.target.value > item.stock) { + event.target.value = item.stock; + } + item.quantity = parseInt(event.target.value); + + SetProductInBasket(id, item.type, item.quantity, modifier); + this.UpdateSubtotal(); + + if (item.quantity === 0) { + delete this.state.items[compositeId]; + + return this.setState({ + ...this.state, + total: GetBasketTotal(), + items: { + ...this.state.items, + }, + }); + } + + // update the total + this.setState({ + ...this.state, + total: GetBasketTotal(), + items: { + ...this.state.items, + [compositeId]: item, + }, + }, false); + + + // update the total so it doesn't need to be rendered again + const total = this.root.querySelector('.basket-total'); + total.innerText = this.state.total; }); }); } diff --git a/src/controllers/set-controller.js b/src/controllers/set-controller.js index 5dc42f0..58a2e1f 100644 --- a/src/controllers/set-controller.js +++ b/src/controllers/set-controller.js @@ -110,7 +110,6 @@ async function SumPrices(setsArray, quantityArray) { } let sum = 0; - console.log(dbres); for (let i = 0; i < dbres.rows.length; i++) { sum += parseFloat(dbres.rows[i].price) * parseFloat(quantityArray[i]); } diff --git a/src/logger.js b/src/logger.js index 0a3e6e5..ab92b36 100644 --- a/src/logger.js +++ b/src/logger.js @@ -32,6 +32,18 @@ function getFormatedTimeString() { // levelSource is the level that the source will log at ie, if levelSource is // LEVEL_WARN, it will only log if the current level is at or above LEVEL_WARN. function internalLog(type, message, cConsoleColour, levelSource) { + message = [message].map(x => { + if (typeof x === 'object') { + return JSON.stringify(x); + } + + if (Array.isArray(x)) { + return x.join(' '); + } + + return x; + }).join(' '); + if (Options.logToConsole && (Options.logLevel <= levelSource)) { console.log(`${getFormatedTimeString()} [${cConsoleColour(type)}] ${message}`); } @@ -48,14 +60,18 @@ function internalLog(type, message, cConsoleColour, levelSource) { } } -const Info = (...messages) => internalLog('INFO', messages.join(' '), clc.greenBright, LEVEL_INFO); -const Warn = (...messages) => internalLog('WARN', messages.join(' '), clc.yellowBright, LEVEL_WARN); -const Error = (...messages) => internalLog('ERROR', messages.join(' '), clc.redBright, LEVEL_STICK); -const Panic = (...messages) => internalLog('PANIC', messages.join(' '), clc.bgRedBright, LEVEL_STICK); -const Debug = (...messages) => internalLog('DEBUG', messages.join(' '), clc.cyanBright, LEVEL_DEBUG); +const Info = (...messages) => internalLog('INFO', messages, clc.greenBright, LEVEL_INFO); +const Warn = (...messages) => internalLog('WARN', messages, clc.yellowBright, LEVEL_WARN); +const Error = (...messages) => internalLog('ERROR', messages, clc.redBright, LEVEL_STICK); +const Panic = (...messages) => internalLog('PANIC', messages, clc.bgRedBright, LEVEL_STICK); +const Debug = (...messages) => internalLog('DEBUG', messages, clc.cyanBright, LEVEL_DEBUG); const Module = (module, ...messages) => internalLog(`MODULE [${module}]`, ` ${messages.join(' ')}`, clc.blue, LEVEL_INFO); const Database = (...messages) => internalLog('PSQL', `[DB] ${messages.join(' ')}`, clc.blue, LEVEL_INFO); const ExpressLogger = (req, res, next) => { + // don't spam the console with 1000000 bloody requests for thumbnails + if (req.originalUrl.startsWith('/api/cdn/')) { + return next(); + } internalLog('HTTP', `[${req.method}] ${req.originalUrl} FROM ${req.headers['x-forwarded-for'] || req.socket.remoteAddress}`, clc.magenta, LEVEL_VERBOSE); next(); }; diff --git a/src/routes/helpers.js b/src/routes/helpers.js index 56102a7..82ac7cf 100644 --- a/src/routes/helpers.js +++ b/src/routes/helpers.js @@ -1,5 +1,6 @@ const BrickController = require('../controllers/brick-controller.js'); const SetController = require('../controllers/set-controller.js'); +const Logger = require('../logger.js'); // AppEng Deadline const EndDate = new Date(1651269600 * 1000); @@ -37,12 +38,20 @@ async function CalculateBasketPrice(req, res) { } } - let setSubtotal = await SetController.SumPrices(setList, setQuantities); - let brickSubtotal = await BrickController.SumPrices(brickList, brickQuantities); + Logger.Debug(`Set list: ${setList} Brick list: ${brickList}`); + let setSubtotal = setList > 0 + ? await SetController.SumPrices(setList, setQuantities) + : 0; + let brickSubtotal = brickList > 0 + ? await BrickController.SumPrices(brickList, brickQuantities) + : 0; + if (setSubtotal.error) setSubtotal = 0; if (brickSubtotal.error) brickSubtotal = 0; + Logger.Debug(`Set subtotal: ${setSubtotal} Brick subtotal: ${brickSubtotal} Total: ${parseFloat(setSubtotal) + parseFloat(brickSubtotal)}`); + res.send({ data: { subtotal: parseFloat(setSubtotal) + parseFloat(brickSubtotal),