Former-commit-id: df0cca4571f50a90d7da755bab48e06c4051bce7
This commit is contained in:
Ben
2022-04-29 04:35:14 +01:00
parent d08904a184
commit b7cf3d44aa
22 changed files with 776 additions and 76 deletions

BIN
README.md

Binary file not shown.

View File

@@ -7,11 +7,13 @@ const AUTH0CONFIG = {
}; };
let auth0 = null; let auth0 = null;
let ready = false;
async function CheckRedirect() { async function CheckRedirect() {
const isAuthenticated = await auth0.isAuthenticated(); const isAuthenticated = await auth0.isAuthenticated();
if (isAuthenticated) { if (isAuthenticated) {
localStorage.setItem('loggedIn', true); localStorage.setItem('loggedIn', true);
ready = true;
return; return;
} }
@@ -23,6 +25,7 @@ async function CheckRedirect() {
} catch (e) { } catch (e) {
window.alert(e.message || 'authentication error, sorry'); window.alert(e.message || 'authentication error, sorry');
localStorage.setItem('loggedIn', false); localStorage.setItem('loggedIn', false);
ready = false;
Signout(); Signout();
} }
@@ -32,9 +35,7 @@ async function CheckRedirect() {
} }
export async function InitAuth0() { export async function InitAuth0() {
// localStorage.setItem('loggedIn', false); ready = false;
// localStorage.setItem('user', 'Guest');
// localStorage.setItem('admin', false);
auth0 = await window.createAuth0Client({ auth0 = await window.createAuth0Client({
domain: AUTH0CONFIG.domain, domain: AUTH0CONFIG.domain,
@@ -50,6 +51,7 @@ export async function InitAuth0() {
localStorage.setItem('user', user.given_name || user.nickname); localStorage.setItem('user', user.given_name || user.nickname);
NotifyNavbar('login', user); NotifyNavbar('login', user);
localStorage.setItem('loggedIn', true); localStorage.setItem('loggedIn', true);
ready = true;
// tell the server about the logon, so that it can make the proper // tell the server about the logon, so that it can make the proper
// entry in the database, if there is for example an address // entry in the database, if there is for example an address
@@ -69,6 +71,11 @@ export async function InitAuth0() {
} }
export async function GetToken() { export async function GetToken() {
/* eslint-disable-next-line */
while (!ready) {
await new Promise(resolve => setTimeout(resolve, 100));
}
const token = await auth0.getTokenSilently(); const token = await auth0.getTokenSilently();
return token; return token;
} }
@@ -90,6 +97,7 @@ export async function LoginSignup() {
export async function Signout() { export async function Signout() {
localStorage.setItem('loggedIn', false); localStorage.setItem('loggedIn', false);
ready = false;
localStorage.setItem('user', 'Guest'); localStorage.setItem('user', 'Guest');
localStorage.setItem('admin', false); localStorage.setItem('admin', false);
await auth0.logout({ await auth0.logout({

View File

@@ -20,7 +20,7 @@
<script type="module" src="/components/search.mjs"></script> <script type="module" src="/components/search.mjs"></script>
<script type="module" src="/components/basket.mjs"></script> <script type="module" src="/components/basket.mjs"></script>
<script type="module" src="/components/basket-popout.mjs"></script> <script type="module" src="/components/basket-popout.mjs"></script>
<script type="module" src="/components/immutable-basket-list.mjs"></script> <script type="module" src="/components/immutable-list.mjs"></script>
<script type="module" src="/components/accessability-popout.mjs"></script> <script type="module" src="/components/accessability-popout.mjs"></script>
<script type="module" src="/components/notificationbar.mjs"></script> <script type="module" src="/components/notificationbar.mjs"></script>
<script type="module" src="/components/tag.mjs"></script> <script type="module" src="/components/tag.mjs"></script>

View File

@@ -19,7 +19,7 @@
<script type="module" src="/components/navbar.mjs"></script> <script type="module" src="/components/navbar.mjs"></script>
<script type="module" src="/components/search.mjs"></script> <script type="module" src="/components/search.mjs"></script>
<script type="module" src="/components/basket-popout.mjs"></script> <script type="module" src="/components/basket-popout.mjs"></script>
<script type="module" src="/components/immutable-basket-list.mjs"></script> <script type="module" src="/components/immutable-list.mjs"></script>
<script type="module" src="/components/checkout.mjs"></script> <script type="module" src="/components/checkout.mjs"></script>
<script type="module" src="/components/accessability-popout.mjs"></script> <script type="module" src="/components/accessability-popout.mjs"></script>
<script type="module" src="/components/notificationbar.mjs"></script> <script type="module" src="/components/notificationbar.mjs"></script>

View File

@@ -66,7 +66,7 @@ class BasketPopout extends Component {
{this.state.total} Items {this.state.total} Items
</div> </div>
<div class="popup-content"> <div class="popup-content">
<immutable-basket-list-component h="400px" class="basket-list"></immutable-basket-list-component> <immutable-list-component source="basket" h="400px" class="basket-list"></immutable-list-component>
</div> </div>
<div class="popup-footer"> <div class="popup-footer">
<span class="popup-footer-total">Subtotal: £${parseFloat(this.state.subtotal).toFixed(2)}</span> <span class="popup-footer-total">Subtotal: £${parseFloat(this.state.subtotal).toFixed(2)}</span>

View File

@@ -79,7 +79,7 @@ class Checkout extends Component {
<div class="checkout-body-right"> <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-title section-title">Your Order <a href="/basket"><span class="edit-basket">edit basket</span><a> </div>
<div class="checkout-summary"> <div class="checkout-summary">
<immutable-basket-list-component h="300px"></immutable-basket-list-component> <immutable-list-component source="basket" h="300px"></immutable-list-component>
<div class="checkout-summary-prices"> <div class="checkout-summary-prices">
<div class="checkout-summary-prices-row"> <div class="checkout-summary-prices-row">
@@ -363,10 +363,10 @@ class Checkout extends Component {
} }
// clear basket // clear basket
await Basket.ClearBasket(); Basket.ClearBasket();
// redirect to receipt // redirect to receipt
window.location.href = `/order/${req.data.receipt_id}`; window.location.href = `/orders/order?id=${req.data.receipt_id}`;
// we're done ! // we're done !
}); });
} }

View File

@@ -32,6 +32,7 @@ export class Component extends HTMLElement {
Update() { } Update() { }
Render() { Component.__WARN('Render'); } Render() { Component.__WARN('Render'); }
OnRender() { } OnRender() { }
OnUnMount() { }
static __IDENTIFY() { Component.__WARN('identify'); } static __IDENTIFY() { Component.__WARN('identify'); }
async connectedCallback() { async connectedCallback() {
@@ -60,6 +61,7 @@ export class Component extends HTMLElement {
disconnectedCallback() { disconnectedCallback() {
this.root.innerHTML = ''; this.root.innerHTML = '';
this.OnUnMount();
} }
watchAttributeChange(callback) { watchAttributeChange(callback) {

View File

@@ -0,0 +1,233 @@
.order-header {
display: flex;
flex-direction: column;
margin-top: 20px;
font-size: 2em;
border-bottom: 1px solid #ccc;
}
.monospace {
font-family: monospace;
background-color: rgb(209, 209, 209);
font-size: 0.9em;
padding: 5px;
}
.order-header-subtitle {
font-size: 0.6em;
margin-bottom: 20px;
margin-top: 10px;
}
.order-body {
margin-top: 20px;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: space-between;
width: 100%;
}
.order-body-item {
display: flex;
flex-direction: column;
width: 100%;
order: 0;
}
.order-breakdown-table {
margin-top: 20px;
width: 100%;
flex-wrap: wrap;
display: flex;
flex-direction: row;
align-items: flex-start;
justify-content: space-between;
}
.order-breakdown-table-item {
display: flex;
flex-direction: column;
width: 30%;
height: 7em;
border: #ccc 1px solid;
/* background-color: #FAFBFB; */
padding: 10px;
margin-bottom: 10px;
box-shadow: 0 0 5px rgba(0,0,0,0.75);
clip-path: inset(0px -5px 0px 0px);
}
.order-breakdown-table-header {
font-size: 1.2em;
font-weight: bold;
color: #444;
margin-bottom: 10px;
text-transform: uppercase;
}
.order-breakdown-table-row {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: flex-start;
width: 100%;
margin-bottom: 10px;
}
.order-breakdown-table-row-value {
font-size: 1em;
margin-bottom: 6px;
}
.order-price-table-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;
}
@media (pointer:none), (pointer:coarse), screen and (max-width: 900px) {
.order-breakdown-table {
flex-direction: column;
}
.order-breakdown-table-item {
width: 100%;
height: auto;
}
}
/* loosely based on https://codepen.io/luisar/pen/JjoOZav */
.order-body-item-table {
width: 100%;
margin-top: 10px;
display: flex;
flex-direction: row;
justify-content: space-between;
}
.order-body-item-left {
width: 49%;
order: 0;
}
.order-body-item-right {
width: 49%;
order: 1;
}
.order-body-header {
margin-top: 20px;
font-size: 1.3em;
padding-bottom: 5px;
border-bottom: 1px solid #ccc;
margin-bottom: 10px;
}
.order-track-step {
display: flex;
height: 7rem;
}
.order-track-step:last-child {
overflow: hidden;
height: 4rem;
}
.order-track-step:last-child .order-track-status div:last-of-type {
display: none;
}
.order-track-status {
margin-right: 1.5rem;
position: relative;
}
.order-track-step-icon {
display: block;
width: 1.8em;
height: 1.8em;
border-radius: 50%;
background: #F2CA52;
}
.order-track-step-line {
display: block;
margin: 0 auto;
width: 4px;
height: 7rem;
background: #F2CA52;
}
.completed {
background: #0EAD69;
}
.order-body-status-title {
transform: translateY(-0.6em);
font-size: 1.2em;
font-weight: 500;
margin-bottom: 3px;
}
.when {
font-size: 1em;
color: #444;
font-weight: lighter;
}
.order-track {
margin-top: 20px;
transition: all .3s height 0.3s;
transform-origin: top center;
}
a {
text-decoration: none;
color: #444;
}
.orders-list-body {
height: 100%;
display: flex;
flex-direction: column;
align-items: flex-start;
width: 100%;
}
.orders-list-item {
display: flex;
flex-direction: column;
margin-top: 30px;
width: 100%;
order: 0;
border-bottom: 1px solid #ccc;
}
.order-list-item-header {
display: flex;
flex-direction: row;
justify-content: space-between;
font-weight: bold;
margin-bottom: 10px;
}
.order-list-item-header-subtitle {
font-weight: lighter;
}
.order-list-item-body {
display: flex;
flex-direction: column;
width: 100%;
order: 0;
}

View File

@@ -1,26 +1,25 @@
import { RegisterComponent, Component } from './components.mjs'; import { RegisterComponent, Component } from './components.mjs';
import { GetBasketTotalPrice } from '../basket.mjs';
import * as LocalStorageListener from '../localstorage-listener.mjs'; import * as LocalStorageListener from '../localstorage-listener.mjs';
class ImmutableBasketList extends Component { // This was changed to be generic from the original: ImmutableBasketList
static __IDENTIFY() { return 'immutable-basket-list'; } // so it acts now on a local storage "source" instead of just basket
// works i guess /shrug
class ImmutableList extends Component {
static __IDENTIFY() { return 'immutable-list'; }
constructor() { constructor() {
super(ImmutableBasketList); super(ImmutableList);
} }
async OnLocalBasketUpdate() { OnLocalStorageListener() {
const basket = localStorage.getItem('basket'); const itemsList = localStorage.getItem(this.state.source);
if (basket) { if (itemsList) {
try { try {
const basketJSON = JSON.parse(basket); const itemsJson = JSON.parse(itemsList);
const subtotal = await GetBasketTotalPrice();
this.setState({ this.setState({
...this.getState, ...this.getState,
items: basketJSON.items, items: itemsJson.items,
total: basketJSON.total,
subtotal,
}); });
} catch (e) { } catch (e) {
console.log(e); console.log(e);
@@ -29,26 +28,17 @@ class ImmutableBasketList extends Component {
this.setState({ this.setState({
...this.getState, ...this.getState,
items: {}, items: {},
total: 0,
subtotal: 0,
}); });
} }
} }
OnMount() { OnMount() {
LocalStorageListener.ListenOnKey('basket', () => { LocalStorageListener.ListenOnKey(this.state.source, () => {
this.OnLocalBasketUpdate(Object.bind(this)); this.OnLocalStorageListener(Object.bind(this));
}); });
this.setState({ this.OnLocalStorageListener(Object.bind(this));
...this.getState,
items: {},
total: 0,
subtotal: 0,
}, false);
this.OnLocalBasketUpdate(Object.bind(this));
} }
Render() { Render() {
@@ -115,4 +105,4 @@ class ImmutableBasketList extends Component {
} }
} }
RegisterComponent(ImmutableBasketList); RegisterComponent(ImmutableList);

View File

@@ -46,22 +46,29 @@ class NavBar extends Component {
return; return;
} }
if (localStorage.admin === 'true' || localStorage.admin === true) {
this.root.querySelector('.stock-mode').style.display = 'flex';
} else {
this.root.querySelector('.stock-mode').style.display = 'none';
}
const account = this.root.querySelector('.account-item'); const account = this.root.querySelector('.account-item');
// doing this with proper dom manipulation wasn't working if (localStorage.admin === 'true' || localStorage.admin === true) {
account.innerHTML = ` this.root.querySelector('.stock-mode').style.display = 'flex';
<a class="nav-link" href="#">${localStorage.user}▾</a> account.innerHTML = `
<ul class="sub-nav" > <a class="nav-link" href="#">${localStorage.user}▾</a>
<li><a class="sub-nav-link" href="#">My Orders</a></li> <ul class="sub-nav" >
<li><a class="sub-nav-link logout-button" href="#">Log Out</a></li> <li><a class="sub-nav-link" href="/orders">My Orders</a></li>
</ul> <li><a class="sub-nav-link" href="">Add or Remove Stock</a></li>
`; <li><a class="sub-nav-link" href="">Review Open Orders</a></li>
<li><a class="sub-nav-link logout-button" href="#">Log Out</a></li>
</ul>
`;
} else {
this.root.querySelector('.stock-mode').style.display = 'none';
account.innerHTML = `
<a class="nav-link" href="#">${localStorage.user}▾</a>
<ul class="sub-nav" >
<li><a class="sub-nav-link" href="/orders">My Orders</a></li>
<li><a class="sub-nav-link logout-button" href="#">Log Out</a></li>
</ul>
`;
}
const logoutButton = account.querySelector('.logout-button'); const logoutButton = account.querySelector('.logout-button');
logoutButton.addEventListener('click', () => { logoutButton.addEventListener('click', () => {

View File

@@ -0,0 +1,60 @@
import { RegisterComponent, Component, SideLoad } from './components.mjs';
import * as Auth from '../auth.mjs';
class OrderList extends Component {
static __IDENTIFY() { return 'order-list'; }
constructor() {
super(OrderList);
}
async OnMount() {
const options = {
method: 'GET',
headers: { Authorization: `Bearer ${await Auth.GetToken()}` },
};
const res = await fetch('/api/auth/orders', options).then(res => res.json());
console.log(res);
this.setState({
...this.getState,
orders: res.data,
}, false);
console.log(this.state);
}
Render() {
return {
template: /* html */`
<div class="order-header">
<span class="order-header-title">Your Orders</span>
</div>
<div class="orders-list-body">
${this.state.orders.map(order => /* html */`
<div class="orders-list-item">
<a href="/orders/order?id=${order.id}"><div class="order-list-item">
<div class="order-list-item-header">
<span class="order-list-item-header-title">Order #${order.id}</span>
<span class="order-list-item-header-subtitle">Placed on ${new Date(order.date_placed).toDateString()}</span>
</div>
<div class="order-list-item-body">
<span class="order-list-item-body-item-title">Paid: £${parseFloat(order.subtotal_paid).toFixed(2)}</span>
<span class="order-list-item-body-item-title">Shipped? ${order.shipped ? 'Yes' : 'No'}</span>
</div>
</div></a>
</div>
`).join('')}
</div>
`,
style: SideLoad('/components/css/order.css'),
};
}
OnRender() {
}
}
RegisterComponent(OrderList);

View File

@@ -0,0 +1,129 @@
import { RegisterComponent, Component, SideLoad } from './components.mjs';
class Order extends Component {
static __IDENTIFY() { return 'order'; }
constructor() {
super(Order);
}
async OnMount() {
// get order id from search param
const query = new URLSearchParams(window.location.search);
const id = query.get('id');
const res = (await fetch(`/api/order/${id}`).then(res => res.json())).data;
this.setState({
...this.getState,
id,
...res,
}, false);
console.log(this.state);
localStorage.setItem('viewing-order', JSON.stringify({ items: res.items }));
}
Render() {
return {
template: /* html */`
<div class="order-header">
<span class="order-header-title">Thank You For Your Order!</span>
<span class="order-header-subtitle">Your order number is <span class="monospace">{this.state.id}</span></span>
</div>
<div class="order-body">
<div class="order-body-item">
<span class="order-body-date-placed">Placed on ${new Date(this.state.date_placed).toDateString()} at ${new Date(this.state.date_placed).toLocaleTimeString()}</span>
</div>
<div class="order-body-item">
<div class="order-breakdown-table">
<div class="order-breakdown-table-item">
<div class="order-breakdown-table-header">Address</div>
<div class="order-breakdown-table-row">
<span class="order-breakdown-table-row-value">John Doe</span>
<span class="order-breakdown-table-row-value">123 Example Av,</span>
<span class="order-breakdown-table-row-value">Portsmouth,</span>
<span class="order-breakdown-table-row-value">PO1 1EA</span>
</div>
</div>
<div class="order-breakdown-table-item">
<div class="order-breakdown-table-header">Payment Card</div>
<div class="order-breakdown-table-row">
<span class="order-breakdown-table-row-value">**** **** **** 1111</span>
<span class="order-breakdown-table-row-value">Expires: 01/20</span>
</div>
</div>
<div class="order-breakdown-table-item">
<div class="order-price-table">
<div class="order-breakdown-table-header">Payment Breakdown</div>
<div class="order-price-table-row">
<span class="order-price-table-row-title">Discount Applied</span>
<span class="order-price-table-row-price">£${parseFloat(this.state.discount).toFixed(2)}</span>
</div>
<div class="order-price-table-row">
<span class="order-price-table-row-title">Total Paid</span>
<span class="order-price-table-row-price">£${parseFloat(this.state.subtotal_paid).toFixed(2)}</span>
</div>
</div>
</div>
</div>
</div>
<div class="order-body-item">
<div class="order-body-item-table">
<div class="order-body-item-left">
<div class="order-body-header">Order Status</div>
<div class="order-track">
<div class="order-track-step">
<span class="order-track-status">
<div class="order-track-step-icon completed"></div>
<div class="order-track-step-line completed"></div>
</span>
<div class="order-track-text">
<span class="order-body-status-title">Ordered</span>
<span class="when">${new Date(this.state.date_placed).toDateString()}</span>
</div>
</div>
<div class="order-track-step">
<span class="order-track-status">
<div class="order-track-step-icon"></div>
<div class="order-track-step-line"></div>
</span>
<div class="order-track-text">
<span class="order-body-status-title">Posted</span>
<span class="when"></span>
</div>
</div>
<div class="order-track-step">
<span class="order-track-status">
<div class="order-track-step-icon"></div>
<div class="order-track-step-line"></div>
</span>
<div class="order-track-text">
<span class="order-body-status-title">Delivered</span>
<span class="when"></span>
</div>
</div>
</div>
</div>
<div class="order-body-item-right">
<div class="order-body-header">Your LegoLog Order</div>
<immutable-list-component source="viewing-order" h="400px" class="order-list"></immutable-list-component>
</div>
</div>
</div>
`,
style: SideLoad('/components/css/order.css'),
};
}
OnRender() {
// todo: add order tracking, the data is already there
}
OnUnMount() {
localStorage.removeItem('viewing-order');
}
}
RegisterComponent(Order);

View File

@@ -23,7 +23,7 @@
<script type="module" src="/components/navbar.mjs"></script> <script type="module" src="/components/navbar.mjs"></script>
<script type="module" src="/components/search.mjs"></script> <script type="module" src="/components/search.mjs"></script>
<script type="module" src="/components/basket-popout.mjs"></script> <script type="module" src="/components/basket-popout.mjs"></script>
<script type="module" src="/components/immutable-basket-list.mjs"></script> <script type="module" src="/components/immutable-list.mjs"></script>
<script type="module" src="/components/accessability-popout.mjs"></script> <script type="module" src="/components/accessability-popout.mjs"></script>
<script type="module" src="/components/notificationbar.mjs"></script> <script type="module" src="/components/notificationbar.mjs"></script>
<script type="module" src="/components/tag.mjs"></script> <script type="module" src="/components/tag.mjs"></script>

View File

@@ -24,7 +24,7 @@
<script type="module" src="/components/search.mjs"></script> <script type="module" src="/components/search.mjs"></script>
<script type="module" src="/components/basket.mjs"></script> <script type="module" src="/components/basket.mjs"></script>
<script type="module" src="/components/basket-popout.mjs"></script> <script type="module" src="/components/basket-popout.mjs"></script>
<script type="module" src="/components/immutable-basket-list.mjs"></script> <script type="module" src="/components/immutable-list.mjs"></script>
<script type="module" src="/components/accessability-popout.mjs"></script> <script type="module" src="/components/accessability-popout.mjs"></script>
<script type="module" src="/components/notificationbar.mjs"></script> <script type="module" src="/components/notificationbar.mjs"></script>
<script type="module" src="/components/storefront.mjs"></script> <script type="module" src="/components/storefront.mjs"></script>

View File

@@ -0,0 +1,37 @@
<html>
<head>
<title>LegoLog Basket</title>
<meta name="viewport">
<link rel="icon" type="image/svg+xml" href="/res/favicon.svg">
<link rel="stylesheet" type="text/css" href="/global.css">
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Londrina+Solid&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Josefin+Sans&display=swap" rel="stylesheet">
<!-- Auth0 - a library for authentication -->
<script src="https://cdn.auth0.com/js/auth0-spa-js/1.13/auth0-spa-js.production.js"></script>
<!-- Components used on this page - they must be included to work -->
<script type="module" src="/components/components.mjs"></script>
<script type="module" src="/components/navbar.mjs"></script>
<script type="module" src="/components/search.mjs"></script>
<script type="module" src="/components/order-list.mjs"></script>
<script type="module" src="/components/basket-popout.mjs"></script>
<script type="module" src="/components/immutable-list.mjs"></script>
<script type="module" src="/components/accessability-popout.mjs"></script>
<script type="module" src="/components/notificationbar.mjs"></script>
<script type="module" src="/components/tag.mjs"></script>
<script type="module" src="/components/super-compact-listing.mjs"></script>
<script type="module" src="/index.mjs"></script>
</head>
<body>
<notificationbar-component></notificationbar-component>
<navbar-component></navbar-component>
<limited-margin>
<order-list-component></order-list-component>
</limited-margin>
</body>

View File

@@ -0,0 +1,37 @@
<html>
<head>
<title>LegoLog Basket</title>
<meta name="viewport">
<link rel="icon" type="image/svg+xml" href="/res/favicon.svg">
<link rel="stylesheet" type="text/css" href="/global.css">
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Londrina+Solid&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Josefin+Sans&display=swap" rel="stylesheet">
<!-- Auth0 - a library for authentication -->
<script src="https://cdn.auth0.com/js/auth0-spa-js/1.13/auth0-spa-js.production.js"></script>
<!-- Components used on this page - they must be included to work -->
<script type="module" src="/components/components.mjs"></script>
<script type="module" src="/components/navbar.mjs"></script>
<script type="module" src="/components/search.mjs"></script>
<script type="module" src="/components/order.mjs"></script>
<script type="module" src="/components/basket-popout.mjs"></script>
<script type="module" src="/components/immutable-list.mjs"></script>
<script type="module" src="/components/accessability-popout.mjs"></script>
<script type="module" src="/components/notificationbar.mjs"></script>
<script type="module" src="/components/tag.mjs"></script>
<script type="module" src="/components/super-compact-listing.mjs"></script>
<script type="module" src="/index.mjs"></script>
</head>
<body>
<notificationbar-component></notificationbar-component>
<navbar-component></navbar-component>
<limited-margin>
<order-component></order-component>
</limited-margin>
</body>

View File

@@ -19,7 +19,7 @@
<script type="module" src="/components/navbar.mjs"></script> <script type="module" src="/components/navbar.mjs"></script>
<script type="module" src="/components/search.mjs"></script> <script type="module" src="/components/search.mjs"></script>
<script type="module" src="/components/basket-popout.mjs"></script> <script type="module" src="/components/basket-popout.mjs"></script>
<script type="module" src="/components/immutable-basket-list.mjs"></script> <script type="module" src="/components/immutable-list.mjs"></script>
<script type="module" src="/components/accessability-popout.mjs"></script> <script type="module" src="/components/accessability-popout.mjs"></script>
<script type="module" src="/components/notificationbar.mjs"></script> <script type="module" src="/components/notificationbar.mjs"></script>
<script type="module" src="/components/storefront.mjs"></script> <script type="module" src="/components/storefront.mjs"></script>

View File

@@ -24,7 +24,7 @@
<script type="module" src="/components/search.mjs"></script> <script type="module" src="/components/search.mjs"></script>
<script type="module" src="/components/basket.mjs"></script> <script type="module" src="/components/basket.mjs"></script>
<script type="module" src="/components/basket-popout.mjs"></script> <script type="module" src="/components/basket-popout.mjs"></script>
<script type="module" src="/components/immutable-basket-list.mjs"></script> <script type="module" src="/components/immutable-list.mjs"></script>
<script type="module" src="/components/accessability-popout.mjs"></script> <script type="module" src="/components/accessability-popout.mjs"></script>
<script type="module" src="/components/notificationbar.mjs"></script> <script type="module" src="/components/notificationbar.mjs"></script>
<script type="module" src="/components/storefront.mjs"></script> <script type="module" src="/components/storefront.mjs"></script>

View File

@@ -90,15 +90,29 @@ CREATE TABLE IF NOT EXISTS users (
date_updated TIMESTAMP WITHOUT TIME ZONE NOT NULL date_updated TIMESTAMP WITHOUT TIME ZONE NOT NULL
); );
CREATE TABLE IF NOT EXISTS offer_code (
code TEXT NOT NULL PRIMARY KEY,
discount DECIMAL NOT NULL, -- percentage or fixed amount
discount_type INT NOT NULL, -- 0 = percentage, 1 = fixed amount
min_order_value DECIMAL NOT NULL,
type TEXT NOT NULL, -- set or brick
expiry_date TIMESTAMP WITHOUT TIME ZONE NOT NULL
);
CREATE TABLE IF NOT EXISTS order_log ( CREATE TABLE IF NOT EXISTS order_log (
id VARCHAR (50) NOT NULL PRIMARY KEY, id VARCHAR (50) NOT NULL PRIMARY KEY,
user_id VARCHAR (50), -- null if guest user_id VARCHAR (50), -- null if guest
offer_code SERIAL,
subtotal_paid DECIMAL NOT NULL, subtotal_paid DECIMAL NOT NULL,
offer_code TEXT,
discount DECIMAL, discount DECIMAL,
date_placed TIMESTAMP WITHOUT TIME ZONE NOT NULL, date_placed TIMESTAMP WITHOUT TIME ZONE NOT NULL,
shipped BOOLEAN NOT NULL,
date_shipped TIMESTAMP WITHOUT TIME ZONE,
recieved BOOLEAN NOT NULL,
date_recieved TIMESTAMP WITHOUT TIME ZONE,
FOREIGN KEY ( user_id ) REFERENCES users( id ), FOREIGN KEY ( user_id ) REFERENCES users( id ),
FOREIGN KEY ( offer_code ) REFERENCES offer_code( id ) FOREIGN KEY ( offer_code ) REFERENCES offer_code( code )
); );
CREATE TABLE IF NOT EXISTS order_item ( CREATE TABLE IF NOT EXISTS order_item (
@@ -108,19 +122,8 @@ CREATE TABLE IF NOT EXISTS order_item (
brick_colour INT, brick_colour INT,
set_id VARCHAR (50), set_id VARCHAR (50),
amount INT NOT NULL, amount INT NOT NULL,
price_paid DECIMAL NOT NULL,
FOREIGN KEY ( order_id ) REFERENCES order_log( id ), FOREIGN KEY ( order_id ) REFERENCES order_log( id ),
FOREIGN KEY ( brick_id ) REFERENCES lego_brick( id ), FOREIGN KEY ( brick_id ) REFERENCES lego_brick( id ),
FOREIGN KEY ( brick_colour ) REFERENCES lego_brick_colour( id ), FOREIGN KEY ( brick_colour ) REFERENCES lego_brick_colour( id ),
FOREIGN KEY ( set_id ) REFERENCES lego_set( id ) FOREIGN KEY ( set_id ) REFERENCES lego_set( id )
); );
CREATE TABLE IF NOT EXISTS offer_code (
id SERIAL NOT NULL PRIMARY KEY,
code TEXT NOT NULL,
discount DECIMAL NOT NULL, -- percentage or fixed amount
discount_type INT NOT NULL, -- 0 = percentage, 1 = fixed amount
min_order_value DECIMAL NOT NULL,
type TEXT NOT NULL, -- set or brick
expiry_date TIMESTAMP WITHOUT TIME ZONE NOT NULL
);

View File

@@ -1,14 +1,154 @@
const Database = require('../database/database.js'); const Database = require('../database/database.js');
const Logger = require('../logger.js'); const Logger = require('../logger.js');
const Crypto = require('crypto');
// C // C
async function NewOrder(userId, total, items, discountId = null, discountApplied = null) {
// generate unique hex order id xxxx-xxxx-xxxx-xxxx "somewhat securely"
const orderId = (Crypto.randomBytes(2).toString('hex') + '-' +
Crypto.randomBytes(2).toString('hex') + '-' +
Crypto.randomBytes(2).toString('hex') + '-' +
Crypto.randomBytes(2).toString('hex')).toUpperCase();
// user_id and discount_id are optional and can be null
await Database.Query('BEGIN TRANSACTION;');
const dbres = await Database.Query(`
INSERT INTO order_log (id, user_id, subtotal_paid, offer_code, discount, date_placed, shipped, recieved)
VALUES ($1, $2, $3, $4, $5, NOW(), FALSE, FALSE)
`, [orderId, userId, total, discountId, discountApplied]).catch(() => {
return {
error: 'Database error',
};
});
let dbresOrderItemError = false;
// this worked first time, it MUST be wrong
for (const [key, item] of Object.entries(items)) {
const brickId = item.type === 'brick' ? key.split('~')[0] : null;
const brickModifier = key.split('~')[1] || null;
const setId = item.type === 'set' ? key : null;
await Database.Query(`
INSERT INTO order_item (order_id, brick_id, brick_colour, set_id, amount)
VALUES ($1, $2, $3, $4, $5)
`, [orderId, brickId, brickModifier, setId, item.quantity]).catch(() => {
dbresOrderItemError = true;
});
}
if (dbres.error || dbresOrderItemError) {
Database.Query('ROLLBACK TRANSACTION;');
Logger.Error('Something went wrong inserting an order into the database');
return {
error: 'Database error',
};
}
Database.Query('COMMIT TRANSACTION;');
return orderId;
}
// R // R
async function GetOrderById(orderid) {
await Database.Query('BEGIN TRANSACTION;');
const dbres = await Database.Query(`
SELECT order_log.id, order_log.user_id, subtotal_paid, offer_code,
discount, date_placed, shipped, date_shipped, recieved,
date_recieved, item.brick_id, item.brick_colour, item.set_id, item.amount
FROM order_log
LEFT JOIN order_item AS item ON item.order_id = order_log.id
WHERE order_log.id = $1
`, [orderid]).catch(() => {
return {
error: 'Database error',
};
});
if (dbres.error) {
Database.Query('ROLLBACK TRANSACTION;');
Logger.Error(dbres.error);
return {
error: 'Database error',
};
}
Database.Query('COMMIT TRANSACTION;');
const result = dbres.rows;
const items = {};
for (const item of result) {
if (item.brick_id) {
items[`${item.brick_id}~${item.brick_colour}`] = {
type: 'brick',
quantity: item.amount,
};
} else if (item.set_id) {
items[item.set_id] = {
type: 'set',
quantity: item.amount,
};
}
}
return {
id: result[0].id,
user_id: result[0].user_id,
subtotal_paid: result[0].subtotal_paid,
offer_code: result[0].offer_code,
discount: result[0].discount,
date_placed: result[0].date_placed,
shipped: result[0].shipped,
date_shipped: result[0].date_shipped,
recieved: result[0].recieved,
date_recieved: result[0].date_recieved,
items,
};
}
async function GetOrdersByUser(userId) {
await Database.Query('BEGIN TRANSACTION;');
const dbres = await Database.Query(`
SELECT order_log.id, order_log.user_id, subtotal_paid,
discount, date_placed, shipped
FROM order_log
WHERE order_log.user_id = $1
`, [userId]).catch(() => {
return {
error: 'Database error',
};
});
if (dbres.error) {
Database.Query('ROLLBACK TRANSACTION;');
Logger.Error(dbres.error);
return {
error: 'Database error',
};
}
Database.Query('COMMIT TRANSACTION;');
const result = dbres.rows;
return result;
}
// U // U
async function OrderShipped(orderid) {
}
async function OrderRecieved(orderid) {
}
// D // D
module.exports = { module.exports = {
NewOrder,
GetOrderById,
GetOrdersByUser,
OrderShipped,
OrderRecieved,
}; };

View File

@@ -25,11 +25,11 @@ function Init() {
Server.App.post('/api/basket/price/', Helpers.CalculateBasketPrice); Server.App.post('/api/basket/price/', Helpers.CalculateBasketPrice);
Server.App.get('/api/discount/', Helpers.DiscountCode); Server.App.get('/api/discount/', Helpers.DiscountCode);
Server.App.post('/api/order/', Order.ProcessNew); Server.App.post('/api/order/', Order.ProcessNew);
Server.App.get('/api/order:id'); Server.App.get('/api/order/:id', Order.GetOrder);
Server.App.get('/api/auth/login/', Auth0.JWTMiddleware, Auth0.Login); Server.App.get('/api/auth/login/', Auth0.JWTMiddleware, Auth0.Login);
Server.App.post('/api/auth/order/', Auth0.JWTMiddleware, Order.ProcessNew); Server.App.post('/api/auth/order/', Auth0.JWTMiddleware, Order.ProcessNew);
Server.App.get('/api/auth/orders/'); Server.App.get('/api/auth/orders/', Auth0.JWTMiddleware, Order.GetOrders);
Logger.Module('API', 'API Routes Initialized'); Logger.Module('API', 'API Routes Initialized');
} }

View File

@@ -2,11 +2,10 @@ const ControllerMaster = require('../controllers/controller-master.js');
const MiscController = require('../controllers/misc-controller.js'); const MiscController = require('../controllers/misc-controller.js');
const BrickController = require('../controllers/brick-controller.js'); const BrickController = require('../controllers/brick-controller.js');
const SetController = require('../controllers/set-controller.js'); const SetController = require('../controllers/set-controller.js');
const OrderController = require('../controllers/order-controller.js');
const AuthRouter = require('./auth0-router.js'); const AuthRouter = require('./auth0-router.js');
async function ProcessNew(req, res) { async function ProcessNew(req, res) {
console.log(req.body);
// as it's optional auth, 0 is guest // as it's optional auth, 0 is guest
let userID = null; let userID = null;
if (req.auth) { if (req.auth) {
@@ -16,8 +15,6 @@ async function ProcessNew(req, res) {
} }
} }
console.log(userID);
// validate the request // validate the request
if (!req.body.basket) { if (!req.body.basket) {
return res.send({ return res.send({
@@ -26,7 +23,7 @@ async function ProcessNew(req, res) {
} }
const basket = req.body.basket; const basket = req.body.basket;
const discountCode = req.body.discountCode || ''; const discountCode = req.body.discountCode || null;
// validate the basket // validate the basket
@@ -114,11 +111,13 @@ async function ProcessNew(req, res) {
// now we need to calculate the discount (if applicable) // now we need to calculate the discount (if applicable)
// again, this could do with some consolidation // again, this could do with some consolidation
let discount = 0; let discount = {
if (discountCode !== '') { discount: 0,
};
if (discountCode !== null) {
const sanatisedCode = ControllerMaster.SanatiseQuery(req.query.code); const sanatisedCode = ControllerMaster.SanatiseQuery(req.query.code);
const discount = await MiscController.GetDiscount(sanatisedCode); discount = await MiscController.GetDiscount(sanatisedCode);
if (discount.error) { if (discount.error) {
return res.send({ return res.send({
@@ -133,9 +132,64 @@ async function ProcessNew(req, res) {
} }
} }
const total = basketSubtotal - discount.discount;
const order = await OrderController.NewOrder(userID, total, basket, discountCode, discount.discount);
if (order.error) {
return res.send({
error: order.error,
});
}
return res.send({
data: {
receipt_id: order,
},
});
}
async function GetOrder(req, res) {
const orderId = req.params.id;
if (!orderId) {
return res.send({
error: 'No order id in request',
});
}
const order = await OrderController.GetOrderById(orderId);
if (order.error) {
return res.send({
error: order.error,
});
}
return res.send({
data: order,
});
}
async function GetOrders(req, res) {
const user = await AuthRouter.Auth0GetUser(req);
const userId = user.sub.split('|')[1];
const orders = await OrderController.GetOrdersByUser(userId);
if (orders.error) {
return res.send({
error: orders.error,
});
}
return res.send({
data: orders,
});
} }
module.exports = { module.exports = {
ProcessNew, ProcessNew,
GetOrder,
GetOrders,
}; };