lol
Former-commit-id: 717ed994ad894a878e3ae21be920b5e71fb3c53a
This commit is contained in:
@@ -7,8 +7,8 @@ import { RegisterComponent, Component } from './components.mjs';
|
||||
// {
|
||||
// "basket": {
|
||||
// "items": {
|
||||
// "item1": amount,
|
||||
// "item2": amount,
|
||||
// "item1~modifier": { quantity, type },
|
||||
// "item2": { quantity, type },
|
||||
// ...
|
||||
// },
|
||||
// "total": total
|
||||
@@ -20,7 +20,7 @@ let basketCallback = null;
|
||||
|
||||
// TODO: Does the localstorage have a problem with mutual exclusion?
|
||||
// TODO: Should the basket be persisted to the server?
|
||||
export function AddProductToBasket(product, type, amount) {
|
||||
export function AddProductToBasket(product, type, amount, brickModifier = 'none') {
|
||||
if (localStorage.getItem('basket') === null || !localStorage.getItem('basket')) {
|
||||
localStorage.setItem('basket', JSON.stringify({
|
||||
items: {},
|
||||
@@ -30,6 +30,10 @@ export function AddProductToBasket(product, type, amount) {
|
||||
|
||||
const basket = JSON.parse(localStorage.getItem('basket'));
|
||||
|
||||
if (type === 'brick') {
|
||||
product += '~' + brickModifier;
|
||||
}
|
||||
|
||||
if (basket.items[product]) {
|
||||
basket.items[product].quantity += amount;
|
||||
} else {
|
||||
@@ -48,12 +52,16 @@ export function AddProductToBasket(product, type, amount) {
|
||||
}
|
||||
}
|
||||
|
||||
export function RemoveProductFromBasket(product, amount) {
|
||||
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] > amount) {
|
||||
basket.items[product] -= amount;
|
||||
} else {
|
||||
|
||||
@@ -26,13 +26,15 @@
|
||||
|
||||
.product-image-container {
|
||||
flex-grow: 1;
|
||||
aspect-ratio: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.active-image {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.product-info {
|
||||
@@ -102,6 +104,22 @@
|
||||
margin-block-end: 0.83em;
|
||||
}
|
||||
|
||||
.brick-colour-selector {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
font-size: 1.3em;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
margin-block-start: 0.83em;
|
||||
margin-block-end: 0.83em;
|
||||
}
|
||||
|
||||
.brick-colour-selector-select {
|
||||
font-size: 1em;
|
||||
font-family: inherit;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.product-quantity-selector {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@@ -254,3 +272,21 @@ input[type=number] {
|
||||
font-size: 2.3em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.brick-colour-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.brick-colour-demonstrator {
|
||||
font-family: inherit;
|
||||
font-size: 1.5em;
|
||||
font-weight: bold;
|
||||
text-shadow: 1px 1px 1px #fff;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin-right: 0.5em;
|
||||
border: #1A1A1A solid 1px;
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@ class ProductListing extends Component {
|
||||
}
|
||||
|
||||
Render() {
|
||||
// set contents for sets
|
||||
let setContents = '';
|
||||
if (this.state.type === 'set') {
|
||||
setContents = /* html */`
|
||||
@@ -66,7 +67,7 @@ class ProductListing extends Component {
|
||||
name="${brick.name}"
|
||||
tag="${brick.tag}"
|
||||
type="brick"
|
||||
price="${brick.price || brick.discount}">
|
||||
price="${brick.discount || brick.price}">
|
||||
</super-compact-listing-component>
|
||||
</div>
|
||||
`).join('')}
|
||||
@@ -75,6 +76,44 @@ class ProductListing extends Component {
|
||||
`;
|
||||
}
|
||||
|
||||
console.log(this.state)
|
||||
|
||||
// brick colour availability for bricks
|
||||
let brickColourAvailability = '';
|
||||
let brickColourSelector = '';
|
||||
if (this.state.type === 'brick') {
|
||||
brickColourAvailability = /* html */`
|
||||
<div class="collapsible-menu">
|
||||
<div class="menu-header">
|
||||
<span class="menu-header-text">Colour Availability</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.colours.map(colour => /* html */`
|
||||
<div class="brick-colour-container">
|
||||
<span class="brick-colour-demonstrator" style="background-color: #${colour.hexrgb}"></span>
|
||||
<span class="brick-colour-name">${colour.name}</span>
|
||||
<span class="brick-colour-types"> In: ${colour.type}</span>
|
||||
</span>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
brickColourSelector = /* html */`
|
||||
<div class="brick-colour-selector">
|
||||
Select Brick Colour
|
||||
<select class="brick-colour-selector-select">
|
||||
${this.state.colours.map(colour => /* html */`
|
||||
<option value="${colour.name}">
|
||||
${colour.type} ${colour.name}
|
||||
</option>
|
||||
`).join('')}
|
||||
</select>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return {
|
||||
template: /* html */`
|
||||
<div class="product-page">
|
||||
@@ -101,6 +140,8 @@ class ProductListing extends Component {
|
||||
: `<span class="product-listing-price">£${parseFloat(this.state.price).toFixed(2)}</span>`}
|
||||
<div class="product-description">${this.state.description || this.state.name + ' ' + this.state.tag}</div>
|
||||
|
||||
${brickColourSelector}
|
||||
|
||||
<div class="product-quantity-selector">
|
||||
<button class="product-quantity-button reduce-quantity" type="button">-</button>
|
||||
<input class="quantity-input" type="number" value="1" min="1" max="{this.state.stock}">
|
||||
@@ -128,6 +169,7 @@ class ProductListing extends Component {
|
||||
</div>
|
||||
|
||||
${setContents}
|
||||
${brickColourAvailability}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -152,9 +194,8 @@ class ProductListing extends Component {
|
||||
quantityInput.value = 1;
|
||||
|
||||
quantityInput.addEventListener('change', () => {
|
||||
if (typeof quantityInput.value !== 'number') {
|
||||
quantityInput.value = 1;
|
||||
}
|
||||
quantityInput.value = parseInt(quantityInput.value);
|
||||
|
||||
if (quantityInput.value > this.state.stock) {
|
||||
quantityInput.value = this.state.stock;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ class Search extends Component {
|
||||
return {
|
||||
template: /* html */`
|
||||
<input id="search-bar" class="menu-item" type="text" placeholder="search..."/>
|
||||
<div class="search-results"></div>
|
||||
`,
|
||||
style: `
|
||||
/* Modified version of https://codepen.io/mihaeltomic/pen/vmwMdm */
|
||||
@@ -56,9 +57,87 @@ class Search extends Component {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.search-results {
|
||||
display: none;
|
||||
background-color: #AB8FFF;
|
||||
flex-wrap: wrap;
|
||||
font-size: 0.6em;
|
||||
position: fixed;
|
||||
width: 65%;
|
||||
}
|
||||
|
||||
#search-bar:focus + .search-results {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.search-results:hover {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.sc-listing {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
}
|
||||
`,
|
||||
};
|
||||
}
|
||||
|
||||
AppendSearchResults(results, empty = false) {
|
||||
const searchResults = this.root.querySelector('.search-results');
|
||||
searchResults.innerHTML = '';
|
||||
if (empty) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const result of results) {
|
||||
const res = /* html */`
|
||||
<super-compact-listing-component class="sc-listing" id="${result.id}"
|
||||
name="${result.name}"
|
||||
tag="${result.tag}"
|
||||
type="${result.type}"
|
||||
price="${result.discount || result.price}">
|
||||
</super-compact-listing-component>
|
||||
`;
|
||||
searchResults.innerHTML += res;
|
||||
}
|
||||
}
|
||||
|
||||
OnRender() {
|
||||
const searchBar = this.root.querySelector('#search-bar');
|
||||
searchBar.value = localStorage.getItem('search-bar');
|
||||
const route = `/api/search?q=${searchBar.value}&per_page=10`;
|
||||
fetch(route).then((response) => {
|
||||
return response.json();
|
||||
}).then((data) => {
|
||||
this.AppendSearchResults(data.data);
|
||||
});
|
||||
|
||||
searchBar.addEventListener('keyup', (e) => {
|
||||
localStorage.setItem('search-bar', e.target.value);
|
||||
|
||||
if (e.target.value === '') {
|
||||
this.AppendSearchResults([], true);
|
||||
return;
|
||||
}
|
||||
|
||||
// we want this to happen async
|
||||
const route = `/api/search?q=${e.target.value}&per_page=10`;
|
||||
fetch(route).then((response) => {
|
||||
return response.json();
|
||||
}).then((data) => {
|
||||
this.AppendSearchResults(data.data);
|
||||
});
|
||||
|
||||
if (e.keyCode === 13) {
|
||||
const searchTerm = searchBar.value;
|
||||
if (searchTerm.length > 0) {
|
||||
window.location.href = `/search?q=${searchTerm}`;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
RegisterComponent(Search);
|
||||
|
||||
@@ -36,7 +36,7 @@ class StoreFront extends Component {
|
||||
<img class="carousel-image" src="res/builder.png" alt="">
|
||||
<div class="carousel-caption">
|
||||
<h1>Our Featured Bonsai Tree Set</h1>
|
||||
<button>Get It Now</button>
|
||||
<a href="/product/?type=set&id=1010&name=Lego%20Bonsai%20Tree"><button>Get It Now</button></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -31,10 +31,13 @@ class SuperCompactProductListing extends Component {
|
||||
style: `
|
||||
.product-listing {
|
||||
width: 95%;
|
||||
background-color: #F5F6F6;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding-left: 10px;
|
||||
padding-right: 15px;
|
||||
margin: 7px;
|
||||
z-index: 0;
|
||||
cursor: pointer;
|
||||
|
||||
@@ -14,12 +14,12 @@ class Tag extends Component {
|
||||
`,
|
||||
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;
|
||||
padding: 0.2em 0.5em;
|
||||
margin-right: 0.3em;
|
||||
margin-top: 0.2em;
|
||||
margin-bottom: 0.2em;
|
||||
line-height: 1.3em;
|
||||
font-weight: bold;
|
||||
background-color: #F2CA52;
|
||||
cursor: pointer;
|
||||
|
||||
@@ -16,15 +16,12 @@
|
||||
<!-- TODO: Going to need to dynamically generate this -->
|
||||
<ul class = "sub-nav">
|
||||
<li><a class="sub-nav-link" href="#">Trending now</a></li>
|
||||
<li><a class="sub-nav-link" href="#">For you</a></li>
|
||||
<li><a class="sub-nav-link" href="#">Sets by theme</a></li>
|
||||
<li><a class="sub-nav-link" href="#">Sets by ages</a></li>
|
||||
<li><a class="sub-nav-link" href="#">Sets by price</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="menu-item dropdown"><a class="nav-link" href="#">Bricks▾</a>
|
||||
<ul class="sub-nav" >
|
||||
<li><a class="sub-nav-link" href="#">For you</a></li>
|
||||
<li><a class="sub-nav-link" href="#">Bricks by catagory</a></li>
|
||||
<li><a class="sub-nav-link" href="#">Bricks by price</a></li>
|
||||
</ul>
|
||||
|
||||
38
docs/API.md
38
docs/API.md
@@ -10,24 +10,24 @@ automatically every request
|
||||
|
||||
| Type | Route | Queries | Auth? | Notes |
|
||||
| --- | --- | --- | -- | --- |
|
||||
| GET | /api/special/ | | no | |
|
||||
| GET | /api/type/:id | | no | |
|
||||
| GET | /api/search/ | query, page | no | Query endpoint |
|
||||
| GET | /api/bricks/ | query, page | no | Query endpoint |
|
||||
| GET | /api/sets/ | query, 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 | |
|
||||
| PUT | /api/auth/login/ | | yes | |
|
||||
| POST | /api/auth/signup/ | | yes | |
|
||||
| GET | /api/auth/orders/ | | yes | |
|
||||
| GET | /api/auth/basket/ | | yes | |
|
||||
| PUT | /api/auth/basket/:id | quantity | yes | |
|
||||
| POST | /api/auth/basket/:id | | yes | manipulate basket content |
|
||||
| DEL | /api/auth/basket/:id | quantity | yes | if no id, delete whole |
|
||||
| DEL | /api/auth/basket/ | | yes | if no id, delete whole |
|
||||
| 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 | |
|
||||
| PUT | /api/auth/login/ | | yes | |
|
||||
| POST | /api/auth/signup/ | | yes | |
|
||||
| GET | /api/auth/orders/ | | yes | |
|
||||
| GET | /api/auth/basket/ | | yes | |
|
||||
| PUT | /api/auth/basket/:id | quantity | yes | |
|
||||
| POST | /api/auth/basket/:id | | yes | manipulate basket content |
|
||||
| DEL | /api/auth/basket/:id | quantity | yes | if no id, delete whole |
|
||||
| DEL | /api/auth/basket/ | | yes | if no id, delete whole |
|
||||
|
||||
Query endpoints do not return the full data on a brick/set, they return
|
||||
a subset for product listing pages
|
||||
@@ -40,6 +40,8 @@ For all endpoints that query, the following parameters are supported:
|
||||
|
||||
tags: tags to include in search
|
||||
|
||||
per_page: results to include per page
|
||||
|
||||
page: starting page
|
||||
|
||||
pages: pages to return starting from page
|
||||
|
||||
@@ -59,8 +59,18 @@ async function GetBrick(brickId) {
|
||||
};
|
||||
}
|
||||
|
||||
const colours = [];
|
||||
for (const colour of colDbres.rows) {
|
||||
colours[colour.name] = {
|
||||
id: colour.id,
|
||||
name: colour.name,
|
||||
hexrgb: colour.hexrgb,
|
||||
type: colour.colour_type,
|
||||
};
|
||||
}
|
||||
|
||||
const brick = dbres.rows[0];
|
||||
brick.colours = colDbres.rows;
|
||||
brick.colours = Object.values(colours);
|
||||
brick.tags = brick.tag.split(',');
|
||||
brick.type = 'brick';
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ const Auth0 = require('./auth0-router.js');
|
||||
function Init() {
|
||||
Server.App.get('/api/special/', Helpers.Special);
|
||||
|
||||
Server.App.get('/api/search/', []);
|
||||
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);
|
||||
|
||||
@@ -16,13 +16,13 @@ function Get(req, res) {
|
||||
id = id.replace('-thumb', '');
|
||||
}
|
||||
|
||||
// work out hash from id
|
||||
const hash = md5(id.split('.png')[0]);
|
||||
const bucket = hash.substring(0, 4);
|
||||
const file = `${process.cwd()}\\db\\img\\${bucket[0]}\\${bucket[1]}\\${bucket[2]}\\${bucket[3]}\\${id}`;
|
||||
|
||||
// this very randomly fails sometimes
|
||||
try {
|
||||
// work out hash from id
|
||||
const hash = md5(id.split('.png')[0]);
|
||||
const bucket = hash.substring(0, 4);
|
||||
const file = `${process.cwd()}\\db\\img\\${bucket[0]}\\${bucket[1]}\\${bucket[2]}\\${bucket[3]}\\${id}`;
|
||||
|
||||
if (fs.existsSync(file)) {
|
||||
if (thumbnail) {
|
||||
// generate thumbnail
|
||||
@@ -34,13 +34,17 @@ function Get(req, res) {
|
||||
.then(data => {
|
||||
res.set('Content-Type', 'image/png');
|
||||
res.send(data);
|
||||
})
|
||||
.catch(err => {
|
||||
Logger.Error(err);
|
||||
res.sendStatus(404);
|
||||
});
|
||||
return;
|
||||
}
|
||||
res.sendFile(file);
|
||||
} else {
|
||||
if (thumbnail) {
|
||||
sharp(`${process.cwd()}\\res\\default.png`)
|
||||
sharp(`${process.cwd()}\\db\\img\\default.png`)
|
||||
.resize({
|
||||
height: 50,
|
||||
}) // keep aspect ratio
|
||||
@@ -48,6 +52,10 @@ function Get(req, res) {
|
||||
.then(data => {
|
||||
res.set('Content-Type', 'image/png');
|
||||
res.send(data);
|
||||
})
|
||||
.catch(err => {
|
||||
Logger.Error(err);
|
||||
res.sendStatus(404);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
|
||||
async function Search(req, res) {
|
||||
const q = req.query.q;
|
||||
console.log(q);
|
||||
res.send(JSON.stringify({
|
||||
data: [
|
||||
{
|
||||
id: '1010',
|
||||
type: 'set',
|
||||
name: q,
|
||||
price: '1',
|
||||
discount: '1',
|
||||
tag: '1',
|
||||
},
|
||||
{
|
||||
id: '1',
|
||||
type: 'brick',
|
||||
name: q,
|
||||
price: '1',
|
||||
discount: '1',
|
||||
tag: '1',
|
||||
},
|
||||
{
|
||||
id: '1',
|
||||
type: 'brick',
|
||||
name: q,
|
||||
price: '1',
|
||||
discount: '1',
|
||||
tag: '1',
|
||||
},
|
||||
{
|
||||
id: '1',
|
||||
type: 'brick',
|
||||
name: q,
|
||||
price: '1',
|
||||
discount: '1',
|
||||
tag: '1',
|
||||
},
|
||||
],
|
||||
}));
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
Search,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user