discounts as well as fixing a dumbb fucking bug
Former-commit-id: 4c082340cebde8ff3434734797e08e8046a63764
This commit is contained in:
109
client/public/basket.mjs
Normal file
109
client/public/basket.mjs
Normal file
@@ -0,0 +1,109 @@
|
||||
// Basket is stored locally only and is not persisted to the server.
|
||||
// It is used to store the current basket and is used to calculate the total price of the basket.
|
||||
// It is also used to store the current user's basket.
|
||||
// The structure of the basket is in local storage and is as follows:
|
||||
// {
|
||||
// "basket": {
|
||||
// "items": {
|
||||
// "item1~modifier": { quantity, type },
|
||||
// "item2": { quantity, type },
|
||||
// ...
|
||||
// },
|
||||
// "total": total
|
||||
// },
|
||||
// }
|
||||
|
||||
// TODO: Does the localstorage have a problem with mutual exclusion?
|
||||
// TODO: Should the basket be persisted to the server?
|
||||
export function GetBasketItems() {
|
||||
if (localStorage.getItem('basket') === null || !localStorage.getItem('basket')) {
|
||||
return;
|
||||
}
|
||||
return JSON.parse(localStorage.getItem('basket')).items;
|
||||
}
|
||||
|
||||
export function AddProductToBasket(product, type, amount, brickModifier = 'none') {
|
||||
if (localStorage.getItem('basket') === null || !localStorage.getItem('basket')) {
|
||||
localStorage.setItem('basket', JSON.stringify({
|
||||
items: {},
|
||||
total: 0,
|
||||
}));
|
||||
}
|
||||
|
||||
const basket = JSON.parse(localStorage.getItem('basket'));
|
||||
|
||||
if (type === 'brick') {
|
||||
product += '~' + brickModifier;
|
||||
}
|
||||
|
||||
if (basket.items[product]) {
|
||||
basket.items[product].quantity += amount;
|
||||
} else {
|
||||
basket.items[product] = {
|
||||
quantity: amount,
|
||||
type,
|
||||
};
|
||||
}
|
||||
|
||||
basket.total += amount;
|
||||
|
||||
localStorage.setItem('basket', JSON.stringify(basket));
|
||||
}
|
||||
|
||||
export function RemoveProductFromBasket(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 (basket.items[product]) {
|
||||
if (basket.items[product].quantity > amount) {
|
||||
basket.items[product].quantity -= amount;
|
||||
} else {
|
||||
delete basket.items[product];
|
||||
}
|
||||
}
|
||||
|
||||
basket.total -= amount;
|
||||
|
||||
localStorage.setItem('basket', JSON.stringify(basket));
|
||||
}
|
||||
|
||||
export function GetBasketTotal() {
|
||||
if (localStorage.getItem('basket') === null || !localStorage.getItem('basket')) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const basket = JSON.parse(localStorage.getItem('basket'));
|
||||
|
||||
return basket.total;
|
||||
}
|
||||
|
||||
export async function GetBasketTotalPrice(discount = 0, type = '£', entity_type = undefined) {
|
||||
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;
|
||||
}
|
||||
|
||||
export async function GetAbsoluteBasketDiscount(discount = 0, type = '£', entity_type = undefined) {
|
||||
|
||||
}
|
||||
@@ -1,120 +1,6 @@
|
||||
import { RegisterComponent, Component } from './components.mjs';
|
||||
|
||||
// Basket is stored locally only and is not persisted to the server.
|
||||
// It is used to store the current basket and is used to calculate the total price of the basket.
|
||||
// It is also used to store the current user's basket.
|
||||
// The structure of the basket is in local storage and is as follows:
|
||||
// {
|
||||
// "basket": {
|
||||
// "items": {
|
||||
// "item1~modifier": { quantity, type },
|
||||
// "item2": { quantity, type },
|
||||
// ...
|
||||
// },
|
||||
// "total": total
|
||||
// },
|
||||
// }
|
||||
|
||||
let basketCallback = null;
|
||||
|
||||
// TODO: Does the localstorage have a problem with mutual exclusion?
|
||||
// TODO: Should the basket be persisted to the server?
|
||||
export function GetBasketItems() {
|
||||
if (localStorage.getItem('basket') === null || !localStorage.getItem('basket')) {
|
||||
return;
|
||||
}
|
||||
return JSON.parse(localStorage.getItem('basket')).items;
|
||||
}
|
||||
|
||||
export function AddProductToBasket(product, type, amount, brickModifier = 'none') {
|
||||
if (localStorage.getItem('basket') === null || !localStorage.getItem('basket')) {
|
||||
localStorage.setItem('basket', JSON.stringify({
|
||||
items: {},
|
||||
total: 0,
|
||||
}));
|
||||
}
|
||||
|
||||
const basket = JSON.parse(localStorage.getItem('basket'));
|
||||
|
||||
if (type === 'brick') {
|
||||
product += '~' + brickModifier;
|
||||
}
|
||||
|
||||
if (basket.items[product]) {
|
||||
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 RemoveProductFromBasket(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 (basket.items[product]) {
|
||||
if (basket.items[product].quantity > amount) {
|
||||
basket.items[product].quantity -= amount;
|
||||
} else {
|
||||
delete basket.items[product];
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const basket = JSON.parse(localStorage.getItem('basket'));
|
||||
|
||||
return basket.total;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
import * as Basket from '../basket.mjs';
|
||||
import * as LocalStorageListener from '../localstorage-listener.mjs';
|
||||
|
||||
class BasketPopout extends Component {
|
||||
static __IDENTIFY() { return 'basket-popout'; }
|
||||
@@ -129,7 +15,7 @@ class BasketPopout extends Component {
|
||||
if (basket) {
|
||||
try {
|
||||
const basketJSON = JSON.parse(basket);
|
||||
const subtotal = await GetBasketTotalPrice();
|
||||
const subtotal = await Basket.GetBasketTotalPrice();
|
||||
this.setState({
|
||||
items: basketJSON.items,
|
||||
total: basketJSON.total,
|
||||
@@ -156,7 +42,7 @@ class BasketPopout extends Component {
|
||||
|
||||
this.OnLocalBasketUpdate(Object.bind(this));
|
||||
|
||||
basketCallback = this.OnLocalBasketUpdate.bind(this);
|
||||
LocalStorageListener.ListenOnKey('basket', this.OnLocalBasketUpdate.bind(this));
|
||||
}
|
||||
|
||||
Render() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { GetBasketItems, AddProductToBasket, RemoveProductFromBasket, GetBasketTotal, GetBasketTotalPrice } from './basket-popout.mjs';
|
||||
import { RegisterComponent, Component } from './components.mjs';
|
||||
import * as BasketMaster from '../basket.mjs';
|
||||
|
||||
class Basket extends Component {
|
||||
static __IDENTIFY() { return 'basket'; }
|
||||
@@ -93,7 +93,7 @@ class Basket extends Component {
|
||||
}
|
||||
|
||||
.basket-item-listing {
|
||||
font-size: 1.3em;
|
||||
font-size: 1.2em;
|
||||
flex-basis: 65%;
|
||||
flex-grow: 4;
|
||||
}
|
||||
@@ -209,7 +209,7 @@ class Basket extends Component {
|
||||
}
|
||||
|
||||
async UpdateSubtotal() {
|
||||
const subtotal = await GetBasketTotalPrice();
|
||||
const subtotal = await BasketMaster.GetBasketTotalPrice();
|
||||
const basketSubtotal = this.root.querySelector('.basket-subtotal');
|
||||
if (basketSubtotal) {
|
||||
basketSubtotal.innerText = parseFloat(subtotal).toFixed(2);
|
||||
@@ -272,34 +272,34 @@ class Basket extends Component {
|
||||
if (event.target.classList.contains('reduce-quantity')) {
|
||||
if (item.quantity > 0) {
|
||||
item.quantity--;
|
||||
RemoveProductFromBasket(id, item.type, 1, modifier);
|
||||
BasketMaster.RemoveProductFromBasket(id, item.type, 1, modifier);
|
||||
}
|
||||
if (item.quantity === 0) {
|
||||
RemoveProductFromBasket(id, item.type, item.quantity, modifier);
|
||||
BasketMaster.RemoveProductFromBasket(id, item.type, item.quantity, modifier);
|
||||
this.UpdateSubtotal();
|
||||
|
||||
return this.setState({
|
||||
...this.state,
|
||||
total: GetBasketTotal(),
|
||||
total: BasketMaster.GetBasketTotal(),
|
||||
items: {
|
||||
...GetBasketItems(),
|
||||
...BasketMaster.GetBasketItems(),
|
||||
},
|
||||
});
|
||||
}
|
||||
} else if (event.target.classList.contains('increase-quantity')) {
|
||||
if (item.quantity < item.stock) {
|
||||
item.quantity++;
|
||||
AddProductToBasket(id, item.type, 1, modifier);
|
||||
BasketMaster.AddProductToBasket(id, item.type, 1, modifier);
|
||||
}
|
||||
} else if (event.target.classList.contains('remove-quantity')) {
|
||||
RemoveProductFromBasket(id, item.type, item.quantity, modifier);
|
||||
BasketMaster.RemoveProductFromBasket(id, item.type, item.quantity, modifier);
|
||||
this.UpdateSubtotal();
|
||||
|
||||
return this.setState({
|
||||
...this.state,
|
||||
total: GetBasketTotal(),
|
||||
total: BasketMaster.GetBasketTotal(),
|
||||
items: {
|
||||
...GetBasketItems(),
|
||||
...BasketMaster.GetBasketItems(),
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -307,7 +307,7 @@ class Basket extends Component {
|
||||
// update the total
|
||||
this.setState({
|
||||
...this.state,
|
||||
total: GetBasketTotal(),
|
||||
total: BasketMaster.GetBasketTotal(),
|
||||
items: {
|
||||
...this.state.items,
|
||||
[compositeId]: item,
|
||||
@@ -349,23 +349,23 @@ class Basket extends Component {
|
||||
}
|
||||
|
||||
if (parseInt(event.target.value) === 0) {
|
||||
RemoveProductFromBasket(id, item.type, item.quantity, modifier);
|
||||
BasketMaster.RemoveProductFromBasket(id, item.type, item.quantity, modifier);
|
||||
|
||||
return this.setState({
|
||||
...this.state,
|
||||
total: GetBasketTotal(),
|
||||
total: BasketMaster.GetBasketTotal(),
|
||||
items: {
|
||||
...GetBasketItems(),
|
||||
...BasketMaster.GetBasketItems(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// if has gone up in quantity then add it
|
||||
if (item.quantity < event.target.value) {
|
||||
AddProductToBasket(id, item.type, event.target.value - item.quantity, modifier);
|
||||
BasketMaster.AddProductToBasket(id, item.type, event.target.value - item.quantity, modifier);
|
||||
item.quantity = parseInt(event.target.value);
|
||||
} else if (item.quantity > event.target.value) {
|
||||
RemoveProductFromBasket(id, item.type, item.quantity - event.target.value, modifier);
|
||||
BasketMaster.RemoveProductFromBasket(id, item.type, item.quantity - event.target.value, modifier);
|
||||
item.quantity = parseInt(event.target.value);
|
||||
}
|
||||
|
||||
@@ -374,7 +374,7 @@ class Basket extends Component {
|
||||
// update the total
|
||||
this.setState({
|
||||
...this.state,
|
||||
total: GetBasketTotal(),
|
||||
total: BasketMaster.GetBasketTotal(),
|
||||
items: {
|
||||
...this.state.items,
|
||||
[compositeId]: item,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { GetBasketTotalPrice } from './basket-popout.mjs';
|
||||
import { GetBasketTotalPrice } from '../basket.mjs';
|
||||
import { RegisterComponent, Component, SideLoad } from './components.mjs';
|
||||
|
||||
class Checkout extends Component {
|
||||
@@ -10,7 +10,9 @@ class Checkout extends Component {
|
||||
|
||||
async OnMount() {
|
||||
this.setState({
|
||||
subtotal: parseFloat(await GetBasketTotalPrice()).toFixed(2),
|
||||
total: parseFloat(await GetBasketTotalPrice()).toFixed(2),
|
||||
discount: 0,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -22,10 +24,20 @@ class Checkout extends Component {
|
||||
</div>
|
||||
<div class="checkout">
|
||||
<div class="checkout-body-left">
|
||||
<div class="checkout-delivery-form-title section-title">Shipping Details</div>
|
||||
<div class="checkout-delivery-form">
|
||||
<div class="checkout-delivery-form-title section-title">Shipping Details</div>
|
||||
<input class="checkout-form-row-input" type="text" autocomplete="address-line1" name="address" placeholder="Shipping Address"/>
|
||||
<input class="checkout-form-row-input" type="text" autocomplete="postal-code" name="postcode" placeholder="Postcode"/>
|
||||
<span class="form-item full-width">
|
||||
<label class="checkout-form-row-label">Email</label>
|
||||
<input class="checkout-form-row-input" type="text" autocomplete="email" name="email" placeholder="Email"/>
|
||||
</span>
|
||||
<span class="form-item full-width">
|
||||
<label class="checkout-form-row-label">Address Line 1</label>
|
||||
<input class="checkout-form-row-input" type="text" autocomplete="address-line1" name="address" placeholder="House Name or Number"/>
|
||||
</span>
|
||||
<span class="form-item full-width">
|
||||
<label class="checkout-form-row-label">Post Code</label>
|
||||
<input class="checkout-form-row-input" type="text" autocomplete="postal-code" name="postcode" placeholder="e.g AB12 CD3"/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="checkout-delivery-form-title section-title">Payment Details</div>
|
||||
@@ -43,7 +55,7 @@ class Checkout extends Component {
|
||||
<div class="payment-row">
|
||||
<span class="form-item">
|
||||
<label class="checkout-form-row-label"> Expiry Date </label>
|
||||
<input class="checkout-form-row-input" type="text" autocomplete="cc-exp" name="cc-exp" placeholder="MM/YY"/>
|
||||
<input class="checkout-form-row-input" type="text" autocomplete="cc-exp" name="cc-exp" placeholder="MM / YY"/>
|
||||
</span>
|
||||
<span class="form-item">
|
||||
<label class="checkout-form-row-label"> CVV / CSC </label>
|
||||
@@ -53,17 +65,36 @@ class Checkout extends Component {
|
||||
</div>
|
||||
|
||||
<div class="checkout-place-order">
|
||||
<button class="checkout-place-order-button">Buy £${this.state.total}</button>
|
||||
<button class="checkout-place-order-button">Buy £${this.state.subtotal - this.state.discount}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="checkout-body-right">
|
||||
<div class="checkout-summary-title section-title">Your Order <a href="/basket"><span class="edit-basket">edit basket</span><a> </div>
|
||||
<div class="checkout-summary">
|
||||
<immutable-basket-list-component h="300px"></immutable-basket-list-component>
|
||||
<div class="checkout-summary-total">Subtotal ${this.state.total}</div>
|
||||
<div class="checkout-summary-total">Shipping (UK Only) ${this.state.total}</div>
|
||||
<div class="checkout-summary-total">Total ${this.state.total}</div>
|
||||
<input type="text" class="offer-text" placeholder="LEGO10"/><button class="offer-button">Apply Offer Code</button>
|
||||
|
||||
<div class="checkout-summary-prices">
|
||||
<div class="checkout-summary-prices-row">
|
||||
<span class="checkout-summary-prices-row-label">Subtotal</span>
|
||||
<span class="checkout-summary-prices-row-value">£${this.state.subtotal}</span>
|
||||
</div>
|
||||
<div class="checkout-summary-prices-row">
|
||||
<span class="checkout-summary-prices-row-label">Delivery</span>
|
||||
<span class="checkout-summary-prices-row-value">£0.00</span>
|
||||
</div>
|
||||
<div class="checkout-summary-prices-row discount-row" style="display: ${this.state.discount > 0 ? 'flex;' : 'none;'}">
|
||||
<span class="checkout-summary-prices-row-label">Discount</span>
|
||||
<span class="checkout-summary-prices-row-value">£-${parseFloat(this.state.discount).toFixed(2)}</span>
|
||||
</div>
|
||||
<div class="checkout-summary-prices-row">
|
||||
<span class="checkout-summary-prices-row-label">Total</span>
|
||||
<span class="checkout-summary-prices-row-value">£${parseFloat(this.state.subtotal - this.state.discount).toFixed(2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div><label class="checkout-form-row-label"> Discount Code </label></div>
|
||||
<div class="checkout-summary-discount-code">
|
||||
<input type="text" class="offer-text" name="offer-text" placeholder="LEGO10"/><button class="offer-button">Apply</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -156,6 +187,58 @@ class Checkout extends Component {
|
||||
lastCvv = e.target.value;
|
||||
}
|
||||
});
|
||||
|
||||
// discount code
|
||||
this.root.querySelector('.offer-button').addEventListener('click', async () => {
|
||||
// get discount code
|
||||
const offerText = this.root.querySelector('input[name="offer-text"]').value;
|
||||
const offerTextBox = this.root.querySelector('.offer-text');
|
||||
|
||||
// check if valid
|
||||
if (offerText.length === 0 || offerTextBox.classList.contains('code-applied')) {
|
||||
// show error
|
||||
offerTextBox.classList.add('error');
|
||||
setTimeout(() => {
|
||||
offerTextBox.classList.remove('error');
|
||||
}, 1000);
|
||||
return;
|
||||
}
|
||||
|
||||
// ask server for discount
|
||||
const req = await fetch(`/api/discount?code=${encodeURIComponent(offerText)}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}).then((res) => res.json());
|
||||
|
||||
if (req.error) {
|
||||
// show error
|
||||
offerTextBox.classList.add('error');
|
||||
setTimeout(() => {
|
||||
offerTextBox.classList.remove('error');
|
||||
}, 1000);
|
||||
return;
|
||||
}
|
||||
|
||||
offerTextBox.classList.add('code-applied');
|
||||
offerTextBox.disabled = true;
|
||||
|
||||
if (await GetBasketTotalPrice() < req.discount.min_value) {
|
||||
// show error
|
||||
offerTextBox.classList.add('error');
|
||||
setTimeout(() => {
|
||||
offerTextBox.classList.remove('error');
|
||||
}, 1000);
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
subtotal: parseFloat(await GetBasketTotalPrice()).toFixed(2),
|
||||
total: parseFloat(await GetBasketTotalPrice(req.discount, req.type, req.entity_type)).toFixed(2),
|
||||
discount: await GetAbsoluteBasketDiscount(req.discount, req.type, req.entity_type),
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,14 +38,16 @@
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* we want the "your order" on top here */
|
||||
.checkout-body-left {
|
||||
width: 100%;
|
||||
order: 1;
|
||||
flex-basis: 100%;
|
||||
}
|
||||
|
||||
.checkout-body-right {
|
||||
width: 100%;
|
||||
margin-top: 50px;
|
||||
order: 0;
|
||||
flex-basis: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,10 +106,128 @@
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.checkout-place-order {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.checkout-place-order-button {
|
||||
width: 100%;
|
||||
box-shadow: #222 0px 0px 2px;
|
||||
background: #222;
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 10px;
|
||||
font-size: 1.2em;
|
||||
cursor: pointer;
|
||||
transition: all 250ms ease-in-out;
|
||||
}
|
||||
|
||||
.edit-basket {
|
||||
font-size: 0.7em;
|
||||
color: #888;
|
||||
float: right;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
|
||||
.checkout-summary-prices {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.checkout-summary-prices-row {
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
font-size: 1em;
|
||||
color: #444;
|
||||
font-weight: lighter;
|
||||
border-bottom: 1px solid #ccc;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.discount-row {
|
||||
color: #E55744;
|
||||
}
|
||||
|
||||
.checkout-summary-discount-code {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.offer-text {
|
||||
margin-bottom: 10px;
|
||||
height: 3em;
|
||||
width: 79%;
|
||||
background-color: #F5F6F6;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 0.4em;
|
||||
transition: all 250ms ease-in-out;
|
||||
}
|
||||
|
||||
.offer-text:focus {
|
||||
outline: 0;
|
||||
border: 2px solid transparent;
|
||||
border-bottom: 2px solid #222;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.offer-button {
|
||||
width: 14%;
|
||||
background: #222;
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 10px;
|
||||
font-size: 1em;
|
||||
cursor: pointer;
|
||||
transition: all 250ms ease-in-out;
|
||||
}
|
||||
|
||||
.offer-button:hover {
|
||||
background-color: #F5F6F6;
|
||||
outline: 2px solid #222;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #E55744;
|
||||
font-size: 0.8em;
|
||||
animation: shake 0.82s cubic-bezier(.36,.07,.19,.97) both;
|
||||
}
|
||||
|
||||
@keyframes shake {
|
||||
10%, 90% {
|
||||
transform: translate3d(-1px, 0, 0);
|
||||
}
|
||||
|
||||
20%, 80% {
|
||||
transform: translate3d(2px, 0, 0);
|
||||
}
|
||||
|
||||
30%, 50%, 70% {
|
||||
transform: translate3d(-4px, 0, 0);
|
||||
}
|
||||
|
||||
40%, 60% {
|
||||
transform: translate3d(4px, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.code-applied {
|
||||
color: #F2CA52;
|
||||
font-weight: bold;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@
|
||||
|
||||
.product-listing-price-new {
|
||||
font-weight: bold;
|
||||
color: red;
|
||||
color: #E55744;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { RegisterComponent, Component } from './components.mjs';
|
||||
import { GetBasketTotalPrice } from './basket-popout.mjs';
|
||||
import { GetBasketTotalPrice } from '../basket.mjs';
|
||||
import * as LocalStorageListener from '../localstorage-listener.mjs';
|
||||
|
||||
class ImmutableBasketList extends Component {
|
||||
@@ -79,8 +79,10 @@ class ImmutableBasketList extends Component {
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: left;
|
||||
height: ${this.state.w || '100%'};
|
||||
height: ${this.state.h || 'auto'};
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
max-width: ${this.state.w || '100%'};
|
||||
max-height: ${this.state.h || 'auto'};
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
justify-content: space-between;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { RegisterComponent, Component, SideLoad } from './components.mjs';
|
||||
import { AddProductToBasket } from './basket-popout.mjs';
|
||||
import { AddProductToBasket } from '../basket.mjs';
|
||||
|
||||
class ProductListing extends Component {
|
||||
static __IDENTIFY() { return 'product-listing'; }
|
||||
|
||||
@@ -109,8 +109,8 @@ class SuperCompactProductListing extends Component {
|
||||
}
|
||||
|
||||
.product-image {
|
||||
max-height: 150px;
|
||||
max-width: 150px;
|
||||
max-height: 110px;
|
||||
max-width: 110px;
|
||||
object-fit: scale-down;
|
||||
object-position: center;
|
||||
}
|
||||
|
||||
35
docs/API.md
35
docs/API.md
@@ -9,21 +9,22 @@ automatically every request
|
||||
## Routes
|
||||
|
||||
| Type | Route | Queries | Auth? | Notes |
|
||||
| --- | --- | --- | -- | --- |
|
||||
| GET | /api/special/ | | no | |
|
||||
| GET | /api/type/:id | | no | |
|
||||
| GET | /api/search/ | query (q), page | no | Query endpoint |
|
||||
| GET | /api/bricks/ | query (q), page | no | Query endpoint |
|
||||
| GET | /api/sets/ | query (q), page | no | Query endpoint |
|
||||
| GET | /api/sets/featured | page | no | Query endpoint |
|
||||
| GET | /api/brick/:id | | no | |
|
||||
| POST | /api/bulk/brick | array | no | POST due to bulk nature |
|
||||
| GET | /api/set/:id | | no | |
|
||||
| GET | /api/cdn/:id | | no | |
|
||||
| GET | /api/basket/price/ | | no | |
|
||||
| PUT | /api/auth/login/ | | yes | |
|
||||
| POST | /api/auth/signup/ | | yes | |
|
||||
| GET | /api/auth/orders/ | | yes | |
|
||||
| --- | --- | --- | - | --- |
|
||||
| GET | /api/special/ | | ❌ | |
|
||||
| GET | /api/search/ | query (q), page | ❌ | Query endpoint |
|
||||
| GET | /api/bricks/ | query (q), page | ❌ | Query endpoint |
|
||||
| GET | /api/sets/ | query (q), page | ❌ | Query endpoint |
|
||||
| GET | /api/sets/featured | page | ❌ | Query endpoint |
|
||||
| GET | /api/brick/:id | | ❌ | |
|
||||
| POST | /api/bulk/brick | array | ❌ | POST due to bulk nature |
|
||||
| GET | /api/set/:id | | ❌ | |
|
||||
| GET | /api/cdn/:id | | ❌ | |
|
||||
| GET | /api/basket/price/ | | ❌ | |
|
||||
| GET | /api/discount/ | offer code | ❌ | |
|
||||
| GET | /api/auth/login/ | | ✔️ | |
|
||||
| POST | /api/auth/order/ | | ✔️❌ | |
|
||||
| GET | /api/auth/order/:id | | ✔️❌ | |
|
||||
| GET | /api/auth/orders/ | | ✔️ | |
|
||||
|
||||
Query endpoints do not return the full data on a brick/set, they return
|
||||
a subset for product listing pages
|
||||
@@ -52,11 +53,11 @@ set: brick to search for (absolute, fuzzy string)
|
||||
|
||||
```js
|
||||
{
|
||||
error: false
|
||||
data: {
|
||||
// defined in the response description for each route
|
||||
}
|
||||
// other important data, or metadata for the data can be added here
|
||||
// other important data, or metadata for the data
|
||||
// (such as pagination data) can be added here
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -91,6 +91,8 @@ async function SumPrices(bricksArr, quantityArray) {
|
||||
}
|
||||
Database.Query('COMMIT TRANSACTION;');
|
||||
|
||||
console.log(dbres.rows)
|
||||
|
||||
// validate database response
|
||||
if (dbres.rows.length === 0) {
|
||||
return {
|
||||
|
||||
@@ -57,7 +57,6 @@ function SanatiseQuery(query) {
|
||||
query = query.trim();
|
||||
query = query.replace(/[^a-zA-Z0-9,&/\s]/g, '');
|
||||
query = escape(query);
|
||||
query = query.toLowerCase();
|
||||
return query;
|
||||
}
|
||||
|
||||
|
||||
40
src/controllers/misc-controller.js
Normal file
40
src/controllers/misc-controller.js
Normal file
@@ -0,0 +1,40 @@
|
||||
const Database = require('../database/database.js');
|
||||
const Logger = require('../logger.js');
|
||||
|
||||
async function GetDiscount(code) {
|
||||
await Database.Query('BEGIN TRANSACTION;');
|
||||
const dbres = await Database.Query(`
|
||||
SELECT discount, discount_type, min_order_value, type
|
||||
FROM offer_code
|
||||
WHERE code = $1;
|
||||
`, [code]).catch(() => {
|
||||
return {
|
||||
error: 'Database error',
|
||||
};
|
||||
});
|
||||
if (dbres.error) {
|
||||
Database.Query('ROLLBACK TRANSACTION;');
|
||||
Logger.Error(dbres.error);
|
||||
return dbres.error;
|
||||
}
|
||||
|
||||
// validate database response
|
||||
if (dbres.rows.length === 0) {
|
||||
return {
|
||||
error: 'Discount code not found',
|
||||
long: 'The discount code you are looking for does not exist',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
discount: dbres.rows[0].discount,
|
||||
type: dbres.rows[0].discount_type === '0' ? '%' : '£',
|
||||
min_value: dbres.rows[0].min_order_value,
|
||||
entity_type: dbres.rows[0].type,
|
||||
end_date: dbres.rows[0].expiry_date,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
GetDiscount,
|
||||
};
|
||||
@@ -13,6 +13,7 @@ function Init() {
|
||||
Server.App.get('/api/special/', Helpers.Special);
|
||||
|
||||
Server.App.get('/api/search/', Query.Search);
|
||||
|
||||
Server.App.get('/api/bricks/', Bricks.Query);
|
||||
Server.App.get('/api/sets/');
|
||||
Server.App.get('/api/sets/featured/', Sets.Featured);
|
||||
@@ -22,12 +23,13 @@ function Init() {
|
||||
|
||||
Server.App.get('/api/cdn/:id', CDN.Get);
|
||||
|
||||
Server.App.get('/api/auth/login', Auth0.JWTMiddleware, Auth0.Login);
|
||||
Server.App.post('/api/basket/price/', Helpers.CalculateBasketPrice);
|
||||
Server.App.get('/api/discount/', Helpers.DiscountCode);
|
||||
|
||||
Server.App.get('/api/auth/login/', Auth0.JWTMiddleware, Auth0.Login);
|
||||
Server.App.get('/api/auth/orders/');
|
||||
Server.App.get('/api/auth/order/:id');
|
||||
|
||||
Server.App.post('/api/basket/price', Helpers.CalculateBasketPrice);
|
||||
|
||||
Logger.Module('API', 'API Routes Initialized');
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
const ControllerMaster = require('../controllers/controller-master.js');
|
||||
const BrickController = require('../controllers/brick-controller.js');
|
||||
const SetController = require('../controllers/set-controller.js');
|
||||
const MiscController = require('../controllers/misc-controller.js');
|
||||
const Logger = require('../logger.js');
|
||||
|
||||
const Delay = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
const EndDate = new Date('2022-06-10T00:00:00.000Z');
|
||||
|
||||
function Special(req, res) {
|
||||
@@ -39,11 +43,26 @@ async function CalculateBasketPrice(req, res) {
|
||||
}
|
||||
}
|
||||
|
||||
// combine bricks by id and quantity into a single array
|
||||
// this annoyingly happens as the brick ids are not unique
|
||||
// when it comes to composite IDs based on modifiers.
|
||||
// As modifiers do not change the price of a brick this is fine.
|
||||
const newBrickList = [];
|
||||
const newBrickQuantities = [];
|
||||
for (let i = 0; i < brickList.length; i++) {
|
||||
if (!newBrickList.includes(brickList[i])) {
|
||||
newBrickList[i] = brickList[i];
|
||||
newBrickQuantities[i] = brickQuantities[i];
|
||||
} else {
|
||||
newBrickQuantities[newBrickList.indexOf(brickList[i])] += brickQuantities[i];
|
||||
}
|
||||
}
|
||||
|
||||
let setSubtotal = setList.length > 0
|
||||
? await SetController.SumPrices(setList, setQuantities)
|
||||
: 0;
|
||||
let brickSubtotal = brickList.length > 0
|
||||
? await BrickController.SumPrices(brickList, brickQuantities)
|
||||
? await BrickController.SumPrices(newBrickList, newBrickQuantities)
|
||||
: 0;
|
||||
|
||||
if (setSubtotal.error) setSubtotal = 0;
|
||||
@@ -59,7 +78,51 @@ async function CalculateBasketPrice(req, res) {
|
||||
}
|
||||
|
||||
|
||||
async function DiscountCode(req, res) {
|
||||
// // artificial delay to simulate a lots of maths
|
||||
// await Delay(1000);
|
||||
|
||||
if (!req.query.code) {
|
||||
res.send({
|
||||
error: 'No code provided',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const sanatisedCode = ControllerMaster.SanatiseQuery(req.query.code);
|
||||
|
||||
const discount = await MiscController.GetDiscount(sanatisedCode);
|
||||
|
||||
|
||||
Logger.Debug(JSON.stringify(discount));
|
||||
|
||||
if (discount.error) {
|
||||
res.send({
|
||||
error: discount.error,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (discount.end_date < new Date()) {
|
||||
res.send({
|
||||
error: 'Discount code expired',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.send({
|
||||
data: {
|
||||
discount: discount.discount,
|
||||
type: discount.type,
|
||||
min_value: discount.min_value,
|
||||
entity_type: discount.entity_type,
|
||||
end_date: discount.end,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
Special,
|
||||
CalculateBasketPrice,
|
||||
DiscountCode,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user