set page
Former-commit-id: e9cbcd159172587181e412bf22ead2260976aca9
This commit is contained in:
@@ -103,7 +103,7 @@ class Basket extends Component {
|
|||||||
|
|
||||||
Render() {
|
Render() {
|
||||||
return {
|
return {
|
||||||
template: `
|
template: /* html */`
|
||||||
<span id="basket-wrapper">
|
<span id="basket-wrapper">
|
||||||
<div class="basket">
|
<div class="basket">
|
||||||
<img id="basket-icon" class="menu-item" src="https://www.svgrepo.com/show/343743/cart.svg" width="50px" stroke="#222" stroke-width="2px" alt="">
|
<img id="basket-icon" class="menu-item" src="https://www.svgrepo.com/show/343743/cart.svg" width="50px" stroke="#222" stroke-width="2px" alt="">
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { RegisterComponent, Component } from './components.mjs';
|
import { RegisterComponent, Component } from './components.mjs';
|
||||||
import * as Helpers from '../helpers.mjs';
|
|
||||||
|
|
||||||
class CompactProductListing extends Component {
|
class CompactProductListing extends Component {
|
||||||
static __IDENTIFY() { return 'compact-listing'; }
|
static __IDENTIFY() { return 'compact-listing'; }
|
||||||
@@ -10,13 +9,13 @@ class CompactProductListing extends Component {
|
|||||||
|
|
||||||
Render() {
|
Render() {
|
||||||
return {
|
return {
|
||||||
template: `
|
template: /* html */`
|
||||||
<div class="product-listing">
|
<div class="product-listing">
|
||||||
<div class="product-listing-image">
|
<div class="product-listing-image">
|
||||||
<img class="product-image"
|
<img class="product-image"
|
||||||
title="Image of {this.state.name}"
|
title="Image of {this.state.name}"
|
||||||
alt="Image of {this.state.name}"
|
alt="Image of {this.state.name}"
|
||||||
src="{this.state.image}">
|
src="/api/cdn/${this.state.id}.png">
|
||||||
</div>
|
</div>
|
||||||
<div class="product-listing-info">
|
<div class="product-listing-info">
|
||||||
<div class="product-listing-name">{this.state.name} {this.state.id}</div>
|
<div class="product-listing-name">{this.state.name} {this.state.id}</div>
|
||||||
|
|||||||
@@ -54,11 +54,8 @@ export class Component extends HTMLElement {
|
|||||||
|
|
||||||
this.Update(Object.bind(this));
|
this.Update(Object.bind(this));
|
||||||
|
|
||||||
this.setState(this.state);
|
this.setState(this.state, false);
|
||||||
|
this.__INVOKE_RENDER(Object.bind(this));
|
||||||
if (this.attributes.length === 0) {
|
|
||||||
this.__INVOKE_RENDER(Object.bind(this));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback() {
|
disconnectedCallback() {
|
||||||
@@ -88,8 +85,9 @@ export class Component extends HTMLElement {
|
|||||||
return this.state;
|
return this.state;
|
||||||
}
|
}
|
||||||
|
|
||||||
setState(newState) {
|
setState(newState, doRender = true) {
|
||||||
this.state = newState;
|
this.state = newState;
|
||||||
|
if (!doRender) return;
|
||||||
this.__INVOKE_RENDER(Object.bind(this));
|
this.__INVOKE_RENDER(Object.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,8 +25,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.product-image-container {
|
.product-image-container {
|
||||||
aspect-ratio: 1;
|
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
max-width: 600px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.active-image {
|
.active-image {
|
||||||
@@ -34,6 +35,15 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.product-info {
|
||||||
|
flex-grow: 2;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-left: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
@media (pointer:none), (pointer:coarse), screen and (max-width: 900px) {
|
@media (pointer:none), (pointer:coarse), screen and (max-width: 900px) {
|
||||||
.product-page, .product-display {
|
.product-page, .product-display {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -55,15 +65,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-info {
|
|
||||||
flex-grow: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: flex-start;
|
|
||||||
margin-left: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-name {
|
.product-name {
|
||||||
font-size: 2em;
|
font-size: 2em;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
@@ -80,16 +81,6 @@
|
|||||||
margin-block-start: 0.83em;
|
margin-block-start: 0.83em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag {
|
|
||||||
padding: 0.3em 1em;
|
|
||||||
margin-right: 1em;
|
|
||||||
line-height: 1.5em;
|
|
||||||
font-size: 0.8em;
|
|
||||||
font-weight: bold;
|
|
||||||
background-color: #F2CA52;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-listing-price {
|
.product-listing-price {
|
||||||
font-size: 1.5em;
|
font-size: 1.5em;
|
||||||
}
|
}
|
||||||
@@ -181,16 +172,16 @@ input[type=number] {
|
|||||||
transform: translateY(4px);
|
transform: translateY(4px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-details-collapsible {
|
.collapsible-menu {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: center;
|
margin-block-end: 2em;
|
||||||
margin-block-end: 3em;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-details-header {
|
.menu-header {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -199,37 +190,67 @@ input[type=number] {
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-bottom: #1A1A1A solid 1px;
|
border-bottom: #1A1A1A solid 1px;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-details-header-arrow {
|
.menu-header-arrow {
|
||||||
transform: rotate(-180deg);
|
transform: rotate(-180deg);
|
||||||
margin-left: 0.5em;
|
margin-left: 0.5em;
|
||||||
transition: transform 0.2s ease-in-out;
|
transition: transform 0.2s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* rotate the arrow down when the details are open */
|
/* rotate the arrow down when the details are open */
|
||||||
.product-details-header-arrow-down {
|
.menu-header-arrow-down {
|
||||||
margin-left: 0.5em;
|
margin-left: 0.5em;
|
||||||
transform: rotate(-90deg);
|
transform: rotate(-90deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-details-content {
|
.menu-content {
|
||||||
visibility: hidden;
|
max-width: fit-content;
|
||||||
width: 100%;
|
display: none;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.details-open {
|
.details-open {
|
||||||
visibility: visible;
|
display: flex;
|
||||||
|
position: static;
|
||||||
|
width: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-details-content-item {
|
.product-details-content-item {
|
||||||
|
padding-top: 0.6em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollable-container {
|
||||||
|
width: auto;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: flex-start;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.set-piece-container {
|
||||||
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-block-start: 0.83em;
|
margin-block-start: 0.83em;
|
||||||
margin-block-end: 0.83em;
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sc-listing {
|
||||||
|
width: 100%;
|
||||||
|
flex-grow: 5;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.set-piece-amount {
|
||||||
|
flex-grow: 1;
|
||||||
|
font-size: 2.3em;
|
||||||
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,14 +36,13 @@ class ProductList extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
template: `
|
template: /* html */`
|
||||||
<h2>{this.state.title}</h2>
|
<h2>{this.state.title}</h2>
|
||||||
<div class="product-list">
|
<div class="product-list">
|
||||||
${this.state.products.data.map(product => {
|
${this.state.products.data.map(product => {
|
||||||
return `<compact-listing-component name="${product.name}"
|
return `<compact-listing-component name="${product.name}"
|
||||||
id="${product.id}"
|
id="${product.id}"
|
||||||
listing="${product.listing}"
|
listing="${product.listing}"
|
||||||
image="${product.image}"
|
|
||||||
price="${product.price}"
|
price="${product.price}"
|
||||||
type="${product.type}"
|
type="${product.type}"
|
||||||
discount="${product.discount || ''}"></compact-listing-component>
|
discount="${product.discount || ''}"></compact-listing-component>
|
||||||
|
|||||||
@@ -13,18 +13,64 @@ class ProductListing extends Component {
|
|||||||
const type = urlParams.get('type');
|
const type = urlParams.get('type');
|
||||||
const id = urlParams.get('id');
|
const id = urlParams.get('id');
|
||||||
|
|
||||||
const getURL = new URL(`/api/${type}/${id}`, document.baseURI);
|
const getProductURL = new URL(`/api/${type}/${id}`, document.baseURI);
|
||||||
const data = await fetch(getURL).then(response => response.json());
|
const productData = await fetch(getProductURL).then(response => response.json());
|
||||||
console.log(data);
|
|
||||||
|
let setContents = [];
|
||||||
|
if (productData.data.type === 'set') {
|
||||||
|
const allPieces = [];
|
||||||
|
Object.keys(productData.data.includedPieces).forEach(key => {
|
||||||
|
allPieces.push(key);
|
||||||
|
});
|
||||||
|
|
||||||
|
const bulkSets = await fetch('/api/bulk/brick', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
ids: allPieces,
|
||||||
|
}),
|
||||||
|
}).then(response => response.json());
|
||||||
|
setContents = bulkSets.data;
|
||||||
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
...this.getState,
|
...this.getState,
|
||||||
...data.data,
|
...productData.data,
|
||||||
});
|
setContents,
|
||||||
|
}, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
Render() {
|
Render() {
|
||||||
|
let setContents = '';
|
||||||
|
console.log(this.state)
|
||||||
|
if (this.state.type === 'set') {
|
||||||
|
setContents = /* html */`
|
||||||
|
<div class="collapsible-menu">
|
||||||
|
<div class="menu-header">
|
||||||
|
<span class="menu-header-text">Set Contents</span>
|
||||||
|
<img class="menu-header-arrow" src="/res/back-arrow.svg" height="30em" alt="down-arrow">
|
||||||
|
</div>
|
||||||
|
<div class="menu-content scrollable-container">
|
||||||
|
${this.state.setContents.map(piece => /* html */`
|
||||||
|
<div class="set-piece-container">
|
||||||
|
<span class="set-piece-amount">x${this.state.includedPieces[piece.id]}</span>
|
||||||
|
<super-compact-listing-component class="sc-listing" id="${piece.id}"
|
||||||
|
name="${piece.name}"
|
||||||
|
tag="${piece.tag}"
|
||||||
|
type="piece"
|
||||||
|
price="${piece.price || piece.discount}">
|
||||||
|
</super-compact-listing-component>
|
||||||
|
</div>
|
||||||
|
`).join('')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
template: `
|
template: /* html */`
|
||||||
<div class="product-page">
|
<div class="product-page">
|
||||||
<div class="back-button">
|
<div class="back-button">
|
||||||
<img class="back-button-svg" src="/res/back-arrow.svg" height="60em" alt="back-arrow">
|
<img class="back-button-svg" src="/res/back-arrow.svg" height="60em" alt="back-arrow">
|
||||||
@@ -39,11 +85,11 @@ class ProductListing extends Component {
|
|||||||
<div class="product-info">
|
<div class="product-info">
|
||||||
<div class="product-tags">
|
<div class="product-tags">
|
||||||
${this.state.tags.map(tag => {
|
${this.state.tags.map(tag => {
|
||||||
return `<span class="tag">${tag}</span>`;
|
return `<tag-component name="${tag}"></tag-component>`;
|
||||||
}).join('')}
|
}).join('')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="product-name">{this.state.name} [{this.state.date_released}]</div>
|
<div class="product-name">{this.state.name} {this.state.id}</div>
|
||||||
${this.state.discount
|
${this.state.discount
|
||||||
? '<span class="product-listing-price-full">£{this.state.price}</span><span class="product-listing-price-new">£{this.state.discount}</span>'
|
? '<span class="product-listing-price-full">£{this.state.price}</span><span class="product-listing-price-new">£{this.state.discount}</span>'
|
||||||
: '<span class="product-listing-price">£{this.state.price}</span>'}
|
: '<span class="product-listing-price">£{this.state.price}</span>'}
|
||||||
@@ -61,34 +107,21 @@ class ProductListing extends Component {
|
|||||||
<img class="add-to-favorites-button" src="https://www.svgrepo.com/show/25921/heart.svg" width="45px" stroke="#222" stroke-width="2px" alt="Add to Favorites" title="Add to Favorites">
|
<img class="add-to-favorites-button" src="https://www.svgrepo.com/show/25921/heart.svg" width="45px" stroke="#222" stroke-width="2px" alt="Add to Favorites" title="Add to Favorites">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="product-details-collapsible">
|
<div class="collapsible-menu">
|
||||||
<div class="product-details-header">
|
<div class="menu-header">
|
||||||
<span class="product-details-header-text">Product Details</span>
|
<span class="menu-header-text">Product Details</span>
|
||||||
<img class="product-details-header-arrow" src="/res/back-arrow.svg" height="30em" alt="down-arrow">
|
<img class="menu-header-arrow" src="/res/back-arrow.svg" height="30em" alt="down-arrow">
|
||||||
</div>
|
</div>
|
||||||
<div class="product-details-content">
|
<div class="menu-content">
|
||||||
<div class="product-details-content-item">
|
<div class="product-details-content-item">Released in {this.state.date_released}</div>
|
||||||
<span class="product-details-date">Released in {this.state.date_released}</span>
|
<div class="product-details-content-item">Dimensions: {this.state.dimensions_x} x {this.state.dimensions_y} x {this.state.dimensions_z}</div>
|
||||||
</div>
|
<div class="product-details-content-item">Weight: {this.state.weight}g</div>
|
||||||
<div class="product-details-content-item">
|
<div class="product-details-content-item">Not suitable for children under the age of 3 years old, small parts are a choking hazard.</div>
|
||||||
<span class="product-details-dimensions">Dimensions: </span>
|
<div class="product-details-content-item">Not for individual resale.</div>
|
||||||
<span class="product-details-dimensions-value">
|
|
||||||
{this.state.dimensions_x} x {this.state.dimensions_y} x {this.state.dimensions_z}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="product-details-content-item">
|
|
||||||
<span class="product-details-weight">Weight: </span>
|
|
||||||
<span class="product-details-weight-value">{this.state.weight}</span>
|
|
||||||
<span class="product-details-weight-unit">g</span>
|
|
||||||
</div>
|
|
||||||
<div class="product-details-content-item">
|
|
||||||
Not suitable for children under the age of 3 years old, small parts are a choking hazard.
|
|
||||||
</div>
|
|
||||||
<div class="product-details-content-item">
|
|
||||||
Not for individual resale.
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
${setContents}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -136,14 +169,15 @@ class ProductListing extends Component {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// product details, collapsable
|
// product details, collapsable
|
||||||
const collapseButton = this.root.querySelector('.product-details-header');
|
const collapseButton = this.root.querySelectorAll('.menu-header');
|
||||||
const collapseContent = this.root.querySelector('.product-details-content');
|
|
||||||
const collapseArrow = this.root.querySelector('.product-details-header-arrow');
|
|
||||||
|
|
||||||
collapseButton.addEventListener('click', () => {
|
collapseButton.forEach(el => el.addEventListener('click', (e) => {
|
||||||
|
const parent = e.path[2].querySelector('.collapsible-menu') ? e.path[1] : e.path[2];
|
||||||
|
const collapseContent = parent.querySelector('.menu-content');
|
||||||
|
const collapseArrow = parent.querySelector('.menu-header-arrow');
|
||||||
collapseContent.classList.toggle('details-open');
|
collapseContent.classList.toggle('details-open');
|
||||||
collapseArrow.classList.toggle('product-details-header-arrow-down');
|
collapseArrow.classList.toggle('menu-header-arrow-down');
|
||||||
});
|
}));
|
||||||
|
|
||||||
// add quantity to basket and then update the basket count
|
// add quantity to basket and then update the basket count
|
||||||
const addToBasket = this.root.querySelector('.add-to-basket-button');
|
const addToBasket = this.root.querySelector('.add-to-basket-button');
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ class Search extends Component {
|
|||||||
|
|
||||||
Render() {
|
Render() {
|
||||||
return {
|
return {
|
||||||
template: `
|
template: /* html */`
|
||||||
<input id="search-bar" class="menu-item" type="text" placeholder="search..."/>
|
<input id="search-bar" class="menu-item" type="text" placeholder="search..."/>
|
||||||
`,
|
`,
|
||||||
style: `
|
style: `
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ class StoreFront extends Component {
|
|||||||
|
|
||||||
Render() {
|
Render() {
|
||||||
return {
|
return {
|
||||||
template: `
|
template: /* html */`
|
||||||
<div class="main-carousel">
|
<div class="main-carousel">
|
||||||
<div class="carousel-cell">
|
<div class="carousel-cell">
|
||||||
<img class="carousel-image" src="/res/lego-image1.jpg" alt="">
|
<img class="carousel-image" src="/res/lego-image1.jpg" alt="">
|
||||||
|
|||||||
87
client/public/components/super-compact-listing.mjs
Normal file
87
client/public/components/super-compact-listing.mjs
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import { RegisterComponent, Component } from './components.mjs';
|
||||||
|
|
||||||
|
class SuperCompactProductListing extends Component {
|
||||||
|
static __IDENTIFY() { return 'super-compact-listing'; }
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(SuperCompactProductListing);
|
||||||
|
}
|
||||||
|
|
||||||
|
Render() {
|
||||||
|
return {
|
||||||
|
template: /* html */`
|
||||||
|
<span class="product-listing">
|
||||||
|
<span class="product-listing-image">
|
||||||
|
<img class="product-image"
|
||||||
|
title="Image of {this.state.name}"
|
||||||
|
alt="Image of {this.state.name}"
|
||||||
|
src="/api/cdn/${this.state.id}-thumb.png">
|
||||||
|
</span>
|
||||||
|
<span class="product-listing-info">
|
||||||
|
<span class="product-listing-name">{this.state.name}</span>
|
||||||
|
<tag-component name="{this.state.tag}"></tag-component>
|
||||||
|
</span>
|
||||||
|
<span class="product-pricing">
|
||||||
|
£{this.state.price}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
`,
|
||||||
|
style: `
|
||||||
|
.product-listing {
|
||||||
|
width: 95%;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
margin: 7px;
|
||||||
|
z-index: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-listing-image {
|
||||||
|
display: block;
|
||||||
|
margin: 0 auto;
|
||||||
|
margin-bottom: 7px;
|
||||||
|
max-width: 100%;
|
||||||
|
flex-grow: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-image {
|
||||||
|
object-fit: scale-down;
|
||||||
|
object-position: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-image:hover {
|
||||||
|
cursor: hand;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-listing-info {
|
||||||
|
border-left: 1px solid #ccc;
|
||||||
|
border-right: 1px solid #ccc;
|
||||||
|
padding-left: 7px;
|
||||||
|
padding-right: 7px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-grow: 50
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-pricing {
|
||||||
|
flex-grow: 1;
|
||||||
|
text-align: right;
|
||||||
|
font-size: 0.8em;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #E55744;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
OnRender() {
|
||||||
|
this.root.addEventListener('click', () => {
|
||||||
|
window.location.href = `/product/?type=${this.state.type}&id=${this.state.id}&name=${encodeURIComponent(this.state.name)}`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RegisterComponent(SuperCompactProductListing);
|
||||||
38
client/public/components/tag.mjs
Normal file
38
client/public/components/tag.mjs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { RegisterComponent, Component } from './components.mjs';
|
||||||
|
|
||||||
|
class Tag extends Component {
|
||||||
|
static __IDENTIFY() { return 'tag'; }
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(Tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
Render() {
|
||||||
|
return {
|
||||||
|
template: /* html */`
|
||||||
|
<span class="tag">{this.state.name}</span>
|
||||||
|
`,
|
||||||
|
style: `
|
||||||
|
.tag {
|
||||||
|
padding: 0.3em 1em;
|
||||||
|
margin-right: 0.3em;
|
||||||
|
margin-top: 0.3em;
|
||||||
|
margin-bottom: 0.3em;
|
||||||
|
line-height: 1.5em;
|
||||||
|
font-size: 0.8em;
|
||||||
|
font-weight: bold;
|
||||||
|
background-color: #F2CA52;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
OnRender() {
|
||||||
|
this.root.addEventListener('click', () => {
|
||||||
|
this.root.classList.toggle('tag-selected');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RegisterComponent(Tag);
|
||||||
@@ -25,7 +25,9 @@
|
|||||||
<script type="module" src="/components/basket.mjs"></script>
|
<script type="module" src="/components/basket.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>
|
||||||
|
<script type="module" src="/components/tag.mjs"></script>
|
||||||
<script type="module" src="/components/product-list.mjs"></script>
|
<script type="module" src="/components/product-list.mjs"></script>
|
||||||
|
<script type="module" src="/components/super-compact-listing.mjs"></script>
|
||||||
<script type="module" src="/components/compact-listing.mjs"></script>
|
<script type="module" src="/components/compact-listing.mjs"></script>
|
||||||
<script type="module" src="/components/product-listing.mjs"></script>
|
<script type="module" src="/components/product-listing.mjs"></script>
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,9 @@
|
|||||||
<script type="module" src="/components/basket.mjs"></script>
|
<script type="module" src="/components/basket.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>
|
||||||
|
<script type="module" src="/components/tag.mjs"></script>
|
||||||
<script type="module" src="/components/product-list.mjs"></script>
|
<script type="module" src="/components/product-list.mjs"></script>
|
||||||
|
<script type="module" src="/components/super-compact-listing.mjs"></script>
|
||||||
<script type="module" src="/components/compact-listing.mjs"></script>
|
<script type="module" src="/components/compact-listing.mjs"></script>
|
||||||
<script type="module" src="/components/product-listing.mjs"></script>
|
<script type="module" src="/components/product-listing.mjs"></script>
|
||||||
|
|
||||||
|
|||||||
@@ -16,9 +16,10 @@ automatically every request
|
|||||||
| GET | /api/bricks/ | query, page | no | Query endpoint |
|
| GET | /api/bricks/ | query, page | no | Query endpoint |
|
||||||
| GET | /api/sets/ | query, page | no | Query endpoint |
|
| GET | /api/sets/ | query, page | no | Query endpoint |
|
||||||
| GET | /api/sets/featured | page | no | Query endpoint |
|
| GET | /api/sets/featured | page | no | Query endpoint |
|
||||||
| GET | /api/brick/:id/ | | no | |
|
| GET | /api/brick/:id | | no | |
|
||||||
| GET | /api/set/:id/ | | no | |
|
| POST | /api/bulk/brick | array | no | POST due to bulk nature |
|
||||||
| GET | /api/cdn/:id/ | | no | |
|
| GET | /api/set/:id | | no | |
|
||||||
|
| GET | /api/cdn/:id | | no | |
|
||||||
| PUT | /api/auth/login/ | | yes | |
|
| PUT | /api/auth/login/ | | yes | |
|
||||||
| POST | /api/auth/signup/ | | yes | |
|
| POST | /api/auth/signup/ | | yes | |
|
||||||
| GET | /api/auth/orders/ | | yes | |
|
| GET | /api/auth/orders/ | | yes | |
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ class MyComponent extends Component {
|
|||||||
|
|
||||||
Render() {
|
Render() {
|
||||||
return {
|
return {
|
||||||
template: `<div>{this.state.name}</div>`,
|
template: /* html */`<div>{this.state.name}</div>`,
|
||||||
style: `div { text-color: red }`,
|
style: `div { text-color: red }`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
1264
package-lock.json
generated
1264
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -27,6 +27,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.25.0",
|
"axios": "^0.25.0",
|
||||||
|
"body-parser": "^1.20.0",
|
||||||
"cli-color": "^2.0.1",
|
"cli-color": "^2.0.1",
|
||||||
"decompress": "^4.2.1",
|
"decompress": "^4.2.1",
|
||||||
"decompress-targz": "^4.1.1",
|
"decompress-targz": "^4.1.1",
|
||||||
@@ -39,7 +40,9 @@
|
|||||||
"moment": "^2.29.1",
|
"moment": "^2.29.1",
|
||||||
"npm": "^8.6.0",
|
"npm": "^8.6.0",
|
||||||
"pg": "^8.7.3",
|
"pg": "^8.7.3",
|
||||||
"pg-native": "^3.0.0"
|
"pg-format": "^1.0.4",
|
||||||
|
"pg-native": "^3.0.0",
|
||||||
|
"sharp": "^0.30.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"eslint": "^8.9.0",
|
"eslint": "^8.9.0",
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
const Database = require('../database/database.js');
|
||||||
|
const PgFormat = require('pg-format');
|
||||||
|
|
||||||
|
async function GetBulkBricks(bricksArr) {
|
||||||
|
await Database.Query('BEGIN TRANSACTION;');
|
||||||
|
const dbres = await Database.Query(PgFormat(`
|
||||||
|
SELECT lego_brick.id, lego_brick.name, tag.name AS "tag", inv.price, inv.new_price AS "discount"
|
||||||
|
FROM lego_brick
|
||||||
|
LEFT JOIN lego_brick_tag AS tags ON tags.brick_id = lego_brick.id
|
||||||
|
LEFT JOIN tag AS tag ON tags.tag = tag.id
|
||||||
|
LEFT JOIN lego_brick_inventory AS inv ON inv.brick_id = lego_brick.id
|
||||||
|
WHERE lego_brick.id IN (%L);
|
||||||
|
`, bricksArr), []);
|
||||||
|
await Database.Query('END TRANSACTION;');
|
||||||
|
|
||||||
|
// validate database response
|
||||||
|
if (dbres.rows.length === 0) {
|
||||||
|
return {
|
||||||
|
error: 'Bricks not found',
|
||||||
|
long: 'The bricks you are looking for do not exist',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const bricks = dbres.rows;
|
||||||
|
// combine tags into a single array
|
||||||
|
for (const brick of bricks) {
|
||||||
|
brick.tags = brick.tag.split(',');
|
||||||
|
}
|
||||||
|
|
||||||
|
return bricks;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function GetBrick(brickId) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
GetBulkBricks,
|
||||||
|
GetBrick,
|
||||||
|
};
|
||||||
|
|||||||
@@ -16,13 +16,15 @@ function ValidateQuery(query) {
|
|||||||
async function GetSet(setId) {
|
async function GetSet(setId) {
|
||||||
await Database.Query('BEGIN TRANSACTION;');
|
await Database.Query('BEGIN TRANSACTION;');
|
||||||
const dbres = await Database.Query(`
|
const dbres = await Database.Query(`
|
||||||
SELECT lego_set.id, lego_set.name, description, tag.name AS "tag", inv.price,
|
SELECT lego_set.id, lego_set.name, description, tag.name AS "tag",
|
||||||
|
set_contents.brick_id, set_contents.amount, inv.price,
|
||||||
date_released, weight, dimensions_x, dimensions_y, dimensions_z,
|
date_released, weight, dimensions_x, dimensions_y, dimensions_z,
|
||||||
new_price AS "discount", inv.stock, inv.last_updated AS "last_stock_update"
|
new_price AS "discount", inv.stock, inv.last_updated AS "last_stock_update"
|
||||||
FROM lego_set
|
FROM lego_set
|
||||||
LEFT JOIN lego_set_inventory AS inv ON inv.set_id = lego_set.id
|
LEFT JOIN lego_set_inventory AS inv ON inv.set_id = lego_set.id
|
||||||
LEFT JOIN lego_set_tag AS tags ON tags.set_id = lego_set.id
|
LEFT JOIN lego_set_tag AS tags ON tags.set_id = lego_set.id
|
||||||
LEFT JOIN tag AS tag ON tags.tag = tag.id
|
LEFT JOIN tag AS tag ON tags.tag = tag.id
|
||||||
|
LEFT JOIN set_descriptor AS set_contents ON set_contents.set_id = lego_set.id
|
||||||
WHERE lego_set.id = $1;
|
WHERE lego_set.id = $1;
|
||||||
`, [setId]);
|
`, [setId]);
|
||||||
await Database.Query('END TRANSACTION;');
|
await Database.Query('END TRANSACTION;');
|
||||||
@@ -36,18 +38,22 @@ async function GetSet(setId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const tags = dbres.rows.reduce((acc, cur) => {
|
const tags = dbres.rows.reduce((acc, cur) => {
|
||||||
acc.push(cur.tag);
|
acc.add(cur.tag);
|
||||||
return acc;
|
return acc;
|
||||||
}, []);
|
}, new Set());
|
||||||
|
|
||||||
|
const pieces = dbres.rows.reduce((acc, cur) => {
|
||||||
|
acc[cur.brick_id] = cur.amount;
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
const set = dbres.rows[0];
|
const set = dbres.rows[0];
|
||||||
delete set.tag;
|
delete set.tag;
|
||||||
set.tags = tags;
|
set.includedPieces = pieces;
|
||||||
|
set.tags = Array.from(tags);
|
||||||
set.image = `/api/cdn/${set.id}.png`;
|
set.image = `/api/cdn/${set.id}.png`;
|
||||||
set.type = 'set';
|
set.type = 'set';
|
||||||
|
|
||||||
console.log(set)
|
|
||||||
|
|
||||||
return set;
|
return set;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,7 +84,6 @@ async function GetSets(page, resPerPage) {
|
|||||||
const sets = dbres.rows;
|
const sets = dbres.rows;
|
||||||
|
|
||||||
for (const set of sets) {
|
for (const set of sets) {
|
||||||
set.image = `/api/cdn/${set.id}.png`;
|
|
||||||
set.type = 'set';
|
set.type = 'set';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ async function Query(query, params, callback) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// debug moment
|
// debug moment
|
||||||
Logger.Database(`PSQL Query: ${query.substring(0, 100)}...`);
|
Logger.Database(`PSQL Query: ${query.substring(0, 500).trim()}...`);
|
||||||
const result = await connection.query(query, params, callback);
|
const result = await connection.query(query, params, callback);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ function Init() {
|
|||||||
Server.App.get('/api/sets/');
|
Server.App.get('/api/sets/');
|
||||||
Server.App.get('/api/sets/featured/', Sets.Featured);
|
Server.App.get('/api/sets/featured/', Sets.Featured);
|
||||||
Server.App.get('/api/brick/:id', Bricks.Get);
|
Server.App.get('/api/brick/:id', Bricks.Get);
|
||||||
|
Server.App.post('/api/bulk/brick', Bricks.GetMultiple);
|
||||||
Server.App.get('/api/set/:id', Sets.Get);
|
Server.App.get('/api/set/:id', Sets.Get);
|
||||||
|
|
||||||
Server.App.get('/api/cdn/:id', CDN.Get);
|
Server.App.get('/api/cdn/:id', CDN.Get);
|
||||||
|
|||||||
@@ -6,35 +6,33 @@ function Get(req, res) {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
function Query(req, res, next) {
|
async function GetMultiple(req, res) {
|
||||||
const query = req.query;
|
if (req.body.ids.length === 0) {
|
||||||
|
res.send(JSON.stringify({
|
||||||
// Validation
|
error: 'No ids provided',
|
||||||
const validation = Controller.ValidateQuery(query);
|
long: 'No ids provided',
|
||||||
if (!validation.isValid) {
|
}));
|
||||||
return res.status(400).json({
|
return;
|
||||||
error: {
|
|
||||||
short: validation.error,
|
|
||||||
long: validation.longError,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Query
|
const bricks = await Controller.GetBulkBricks(req.body.ids);
|
||||||
Controller.Query(query, (err, data) => {
|
|
||||||
if (err) {
|
|
||||||
return res.status(500).json({
|
|
||||||
error: err,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json(data);
|
if (bricks.error) {
|
||||||
});
|
res.send(JSON.stringify(bricks));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.send(JSON.stringify({
|
||||||
|
data: bricks,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function Query(req, res, next) {
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
Get,
|
Get,
|
||||||
|
GetMultiple,
|
||||||
Query,
|
Query,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,19 +1,61 @@
|
|||||||
|
const Logger = require('../logger.js');
|
||||||
|
|
||||||
const md5 = require('md5');
|
const md5 = require('md5');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
|
|
||||||
|
// fast thumbnail generation
|
||||||
|
const sharp = require('sharp');
|
||||||
|
|
||||||
function Get(req, res) {
|
function Get(req, res) {
|
||||||
// get id from url
|
// get id from url
|
||||||
const id = req.params.id;
|
let id = req.params.id;
|
||||||
|
|
||||||
|
let thumbnail = false;
|
||||||
|
if (id.includes('-thumb')) {
|
||||||
|
thumbnail = true;
|
||||||
|
id = id.replace('-thumb', '');
|
||||||
|
}
|
||||||
|
|
||||||
// work out hash from id
|
// work out hash from id
|
||||||
const hash = md5(id.split('.png')[0]);
|
const hash = md5(id.split('.png')[0]);
|
||||||
const bucket = hash.substring(0, 4);
|
const bucket = hash.substring(0, 4);
|
||||||
const file = `${process.cwd()}\\db\\img\\${bucket[0]}\\${bucket[1]}\\${bucket[2]}\\${bucket[3]}\\${id}`;
|
const file = `${process.cwd()}\\db\\img\\${bucket[0]}\\${bucket[1]}\\${bucket[2]}\\${bucket[3]}\\${id}`;
|
||||||
|
|
||||||
if (fs.existsSync(file)) {
|
// this very randomly fails sometimes
|
||||||
res.sendFile(file);
|
try {
|
||||||
} else {
|
if (fs.existsSync(file)) {
|
||||||
res.sendFile(`${process.cwd()}\\db\\img\\default.png`);
|
if (thumbnail) {
|
||||||
|
// generate thumbnail
|
||||||
|
sharp(file)
|
||||||
|
.resize({
|
||||||
|
height: 50,
|
||||||
|
}) // keep aspect ratio
|
||||||
|
.toBuffer()
|
||||||
|
.then(data => {
|
||||||
|
res.set('Content-Type', 'image/png');
|
||||||
|
res.send(data);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.sendFile(file);
|
||||||
|
} else {
|
||||||
|
if (thumbnail) {
|
||||||
|
sharp(`${process.cwd()}\\res\\default.png`)
|
||||||
|
.resize({
|
||||||
|
height: 50,
|
||||||
|
}) // keep aspect ratio
|
||||||
|
.toBuffer()
|
||||||
|
.then(data => {
|
||||||
|
res.set('Content-Type', 'image/png');
|
||||||
|
res.send(data);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.sendFile(`${process.cwd()}\\db\\img\\default.png`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
Logger.Error(err);
|
||||||
|
res.sendStatus(404);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
const Logger = require('../logger.js');
|
const Logger = require('../logger.js');
|
||||||
|
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
|
const bodyParser = require('body-parser');
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
function listen(port) {
|
function listen(port) {
|
||||||
@@ -10,6 +11,9 @@ function listen(port) {
|
|||||||
Logger.Info('Setting up basic middleware...');
|
Logger.Info('Setting up basic middleware...');
|
||||||
|
|
||||||
app.use(Logger.ExpressLogger);
|
app.use(Logger.ExpressLogger);
|
||||||
|
app.use(bodyParser.urlencoded({ extended: true }));
|
||||||
|
app.use(bodyParser.json());
|
||||||
|
|
||||||
app.use(express.static('client/public/'));
|
app.use(express.static('client/public/'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user