happy with the search feature
Former-commit-id: 9d9e3a7a7b330392f9d6d1c65439ea86e45ed03c
This commit is contained in:
42
client/public/about/index.html
Normal file
42
client/public/about/index.html
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>LegoLog: About</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 type="module" src="https://cdn.auth0.com/js/auth0-spa-js/1.13/auth0-spa-js.production.js"></script>
|
||||||
|
|
||||||
|
<!-- Flickity - a library to make carousells easier to implement -->
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/flickity@2/dist/flickity.min.css">
|
||||||
|
<script src="https://unpkg.com/flickity@2/dist/flickity.pkgd.min.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/basket.mjs"></script>
|
||||||
|
<script type="module" src="/components/notificationbar.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/super-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="index.mjs"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<notificationbar-component></notificationbar-component>
|
||||||
|
<navbar-component></navbar-component>
|
||||||
|
<limited-margin>
|
||||||
|
|
||||||
|
</limited-margin>
|
||||||
|
</body>
|
||||||
@@ -7,21 +7,31 @@ class ProductList extends Component {
|
|||||||
super(ProductList);
|
super(ProductList);
|
||||||
}
|
}
|
||||||
|
|
||||||
async OnMount() {
|
async FetchListings(from) {
|
||||||
const route = this.state.getroute;
|
const products = await fetch(from).then(response => response.json());
|
||||||
const products = await fetch(route).then(response => response.json());
|
const productsList = this.state.products || [];
|
||||||
|
// concat the new products to the existing products
|
||||||
|
const newProducts = productsList.concat(products.data);
|
||||||
this.setState({
|
this.setState({
|
||||||
...this.getState,
|
...this.getState,
|
||||||
products,
|
products: newProducts,
|
||||||
current_page: products.page.current_page,
|
page: products.page.page,
|
||||||
last_page: products.page.last_page,
|
per_page: products.page.per_page,
|
||||||
total: products.page.total,
|
total: products.page.total,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Render() {
|
OnMount() {
|
||||||
|
this.loading = false;
|
||||||
this.keepLoading = false;
|
this.keepLoading = false;
|
||||||
if (this.state.current_page >= this.state.last_page) {
|
|
||||||
|
const route = this.state.getroute;
|
||||||
|
this.FetchListings(route);
|
||||||
|
this.state.products = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
Render() {
|
||||||
|
if (this.state.page * this.state.per_page >= this.state.total) {
|
||||||
this.keepLoading = false;
|
this.keepLoading = false;
|
||||||
this.loadingBar = '';
|
this.loadingBar = '';
|
||||||
} else {
|
} else {
|
||||||
@@ -39,7 +49,7 @@ class ProductList extends Component {
|
|||||||
template: /* html */`
|
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.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}"
|
||||||
@@ -151,13 +161,31 @@ class ProductList extends Component {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
OnRender() {
|
OnRender() {
|
||||||
|
this.loading = false;
|
||||||
// scroll to bottom event listener
|
// scroll to bottom event listener
|
||||||
if (this.keepLoading) {
|
if (this.keepLoading) {
|
||||||
window.addEventListener('scroll', () => {
|
window.addEventListener('scroll', async () => {
|
||||||
if (window.innerHeight + window.scrollY >= document.body.offsetHeight) {
|
// start loading 200px before the bottom of the page
|
||||||
console.log('scrolled to bottom');
|
if (window.innerHeight + window.scrollY >= document.body.offsetHeight - 300) {
|
||||||
|
if (this.loading) return;
|
||||||
|
if (this.state.page * this.state.per_page >= this.state.total) {
|
||||||
|
this.keepLoading = false;
|
||||||
|
this.loadingBar = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
|
||||||
|
// parse the getRoute as a query string
|
||||||
|
const getRoute = this.state.getroute;
|
||||||
|
// split into query and location
|
||||||
|
const [locationStr, queryStr] = getRoute.split('?');
|
||||||
|
|
||||||
|
const query = new URLSearchParams(queryStr);
|
||||||
|
query.append('page', parseInt(this.state.page) + 1);
|
||||||
|
|
||||||
|
await this.FetchListings(`${locationStr}?${query.toString()}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,8 +63,16 @@ class Search extends Component {
|
|||||||
background-color: #AB8FFF;
|
background-color: #AB8FFF;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
font-size: 0.6em;
|
font-size: 0.6em;
|
||||||
position: fixed;
|
position: absolute;
|
||||||
width: 65%;
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (pointer:none), (pointer:coarse), screen and (max-width: 900px) {
|
||||||
|
.search-results {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#search-bar:focus + .search-results {
|
#search-bar:focus + .search-results {
|
||||||
@@ -104,15 +112,28 @@ class Search extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
OnRender() {
|
GetSearch(value) {
|
||||||
const searchBar = this.root.querySelector('#search-bar');
|
if (value === '') {
|
||||||
searchBar.value = localStorage.getItem('search-bar');
|
this.AppendSearchResults([], true);
|
||||||
const route = `/api/search?q=${searchBar.value}&per_page=10`;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const route = `/api/search?q=${value}&per_page=10`;
|
||||||
fetch(route).then((response) => {
|
fetch(route).then((response) => {
|
||||||
return response.json();
|
return response.json();
|
||||||
}).then((data) => {
|
}).then((data) => {
|
||||||
|
if (data.error) {
|
||||||
|
this.AppendSearchResults([], true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.AppendSearchResults(data.data);
|
this.AppendSearchResults(data.data);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
OnRender() {
|
||||||
|
const searchBar = this.root.querySelector('#search-bar');
|
||||||
|
searchBar.value = localStorage.getItem('search-bar');
|
||||||
|
this.GetSearch(searchBar.value);
|
||||||
|
|
||||||
searchBar.addEventListener('keyup', (e) => {
|
searchBar.addEventListener('keyup', (e) => {
|
||||||
localStorage.setItem('search-bar', e.target.value);
|
localStorage.setItem('search-bar', e.target.value);
|
||||||
@@ -123,15 +144,10 @@ class Search extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// we want this to happen async
|
// we want this to happen async
|
||||||
const route = `/api/search?q=${e.target.value}&per_page=10`;
|
this.GetSearch(e.target.value);
|
||||||
fetch(route).then((response) => {
|
|
||||||
return response.json();
|
|
||||||
}).then((data) => {
|
|
||||||
this.AppendSearchResults(data.data);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (e.keyCode === 13) {
|
if (e.keyCode === 13) {
|
||||||
const searchTerm = searchBar.value;
|
const searchTerm = e.target.value;
|
||||||
if (searchTerm.length > 0) {
|
if (searchTerm.length > 0) {
|
||||||
window.location.href = `/search?q=${searchTerm}`;
|
window.location.href = `/search?q=${searchTerm}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,21 +15,21 @@ class StoreFront extends Component {
|
|||||||
<img class="carousel-image" src="/res/lego-image1.jpg" alt="">
|
<img class="carousel-image" src="/res/lego-image1.jpg" alt="">
|
||||||
<div class="carousel-caption">
|
<div class="carousel-caption">
|
||||||
<h1>Welcome to LegoLog!</h1>
|
<h1>Welcome to LegoLog!</h1>
|
||||||
<button>Shop LEGO® Now</button>
|
<a href="/featured/"><button>Shop LEGO® Now</button></a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="carousel-cell">
|
<div class="carousel-cell">
|
||||||
<img class="carousel-image" src="res/warehouse.png" alt="">
|
<img class="carousel-image" src="res/warehouse.png" alt="">
|
||||||
<div class="carousel-caption">
|
<div class="carousel-caption">
|
||||||
<h1>Our state of the art warehouse ensures your speedy delivery</h1>
|
<h1>Our state of the art warehouse ensures your speedy delivery</h1>
|
||||||
<button>Find Out More</button>
|
<a href="/about/"><button>Find Out More</button></a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="carousel-cell">
|
<div class="carousel-cell">
|
||||||
<img class="carousel-image" src="res/space.png" alt="">
|
<img class="carousel-image" src="res/space.png" alt="">
|
||||||
<div class="carousel-caption">
|
<div class="carousel-caption">
|
||||||
<h1>NASA's Shuttle Discovery</h1>
|
<h1>NASA's Shuttle Discovery</h1>
|
||||||
<button>Shop Space Now</button>
|
<a href="/search/?q=nasa"><button>Shop Space Now</button></a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="carousel-cell">
|
<div class="carousel-cell">
|
||||||
|
|||||||
46
client/public/featured/index.html
Normal file
46
client/public/featured/index.html
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>LegoLog: Featured Sets</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 type="module" src="https://cdn.auth0.com/js/auth0-spa-js/1.13/auth0-spa-js.production.js"></script>
|
||||||
|
|
||||||
|
<!-- Flickity - a library to make carousells easier to implement -->
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/flickity@2/dist/flickity.min.css">
|
||||||
|
<script src="https://unpkg.com/flickity@2/dist/flickity.pkgd.min.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/basket.mjs"></script>
|
||||||
|
<script type="module" src="/components/notificationbar.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/super-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="index.mjs"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<notificationbar-component></notificationbar-component>
|
||||||
|
<navbar-component></navbar-component>
|
||||||
|
<limited-margin>
|
||||||
|
<product-list-component id="featured"
|
||||||
|
title="Featured Lego Sets"
|
||||||
|
getroute="/api/sets/featured"
|
||||||
|
type="set">
|
||||||
|
</product-list-component>
|
||||||
|
</limited-margin>
|
||||||
|
</body>
|
||||||
@@ -38,20 +38,5 @@
|
|||||||
<navbar-component></navbar-component>
|
<navbar-component></navbar-component>
|
||||||
<limited-margin>
|
<limited-margin>
|
||||||
<storefront-component id="store"></storefront-component>
|
<storefront-component id="store"></storefront-component>
|
||||||
|
|
||||||
<!-- <canvas id="webglviewer" width="300" height="300"></canvas> -->
|
|
||||||
<!-- <canvas id="webglviewer" width="300" height="300"></canvas>
|
|
||||||
<canvas id="webglviewer" width="300" height="300"></canvas>
|
|
||||||
<canvas id="webglviewer" width="300" height="300"></canvas>
|
|
||||||
<canvas id="webglviewer" width="300" height="300"></canvas>
|
|
||||||
<canvas id="webglviewer" width="300" height="300"></canvas>
|
|
||||||
<canvas id="webglviewer" width="300" height="300"></canvas>
|
|
||||||
<canvas id="webglviewer" width="300" height="300"></canvas>
|
|
||||||
<canvas id="webglviewer" width="300" height="300"></canvas>
|
|
||||||
<canvas id="webglviewer" width="300" height="300"></canvas>
|
|
||||||
<canvas id="webglviewer" width="300" height="300"></canvas>
|
|
||||||
<canvas id="webglviewer" width="300" height="300"></canvas> -->
|
|
||||||
|
|
||||||
|
|
||||||
</limited-margin>
|
</limited-margin>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>LegoLog Home!</title>
|
<title>LegoLog!</title>
|
||||||
<meta name="viewport">
|
<meta name="viewport">
|
||||||
<link rel="icon" type="image/svg+xml" href="/res/favicon.svg">
|
<link rel="icon" type="image/svg+xml" href="/res/favicon.svg">
|
||||||
<link rel="stylesheet" type="text/css" href="/global.css">
|
<link rel="stylesheet" type="text/css" href="../global.css">
|
||||||
|
|
||||||
<!-- Fonts -->
|
<!-- Fonts -->
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
@@ -34,19 +34,5 @@
|
|||||||
<navbar-component></navbar-component>
|
<navbar-component></navbar-component>
|
||||||
<limited-margin>
|
<limited-margin>
|
||||||
<product-listing-component></product-listing-component>
|
<product-listing-component></product-listing-component>
|
||||||
|
|
||||||
<!-- <canvas id="webglviewer" width="300" height="300"></canvas> -->
|
|
||||||
<!-- <canvas id="webglviewer" width="300" height="300"></canvas>
|
|
||||||
<canvas id="webglviewer" width="300" height="300"></canvas>
|
|
||||||
<canvas id="webglviewer" width="300" height="300"></canvas>
|
|
||||||
<canvas id="webglviewer" width="300" height="300"></canvas>
|
|
||||||
<canvas id="webglviewer" width="300" height="300"></canvas>
|
|
||||||
<canvas id="webglviewer" width="300" height="300"></canvas>
|
|
||||||
<canvas id="webglviewer" width="300" height="300"></canvas>
|
|
||||||
<canvas id="webglviewer" width="300" height="300"></canvas>
|
|
||||||
<canvas id="webglviewer" width="300" height="300"></canvas>
|
|
||||||
<canvas id="webglviewer" width="300" height="300"></canvas>
|
|
||||||
<canvas id="webglviewer" width="300" height="300"></canvas> -->
|
|
||||||
|
|
||||||
</limited-margin>
|
</limited-margin>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
54
client/public/search/index.html
Normal file
54
client/public/search/index.html
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>LegoLog: Featured Sets</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 type="module" src="https://cdn.auth0.com/js/auth0-spa-js/1.13/auth0-spa-js.production.js"></script>
|
||||||
|
|
||||||
|
<!-- Flickity - a library to make carousells easier to implement -->
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/flickity@2/dist/flickity.min.css">
|
||||||
|
<script src="https://unpkg.com/flickity@2/dist/flickity.pkgd.min.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/basket.mjs"></script>
|
||||||
|
<script type="module" src="/components/notificationbar.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/super-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="index.mjs"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<notificationbar-component></notificationbar-component>
|
||||||
|
<navbar-component></navbar-component>
|
||||||
|
<limited-margin>
|
||||||
|
<script>
|
||||||
|
console.log(`/api/search/${window.location.search}`)
|
||||||
|
// parse out the search query using the query object
|
||||||
|
const query = new URLSearchParams(window.location.search)
|
||||||
|
// get the search query
|
||||||
|
const searchQuery = query.get('q')
|
||||||
|
document.write(/* html */`
|
||||||
|
<product-list-component id="results"
|
||||||
|
title="Search Results for '${searchQuery}'"
|
||||||
|
getroute="/api/search/${window.location.search}">
|
||||||
|
</product-list-component>
|
||||||
|
`);
|
||||||
|
</script>
|
||||||
|
</limited-margin>
|
||||||
|
</body>
|
||||||
10
docs/API.md
10
docs/API.md
@@ -40,17 +40,17 @@ For all endpoints that query, the following parameters are supported:
|
|||||||
|
|
||||||
tags: tags to include in search
|
tags: tags to include in search
|
||||||
|
|
||||||
|
total: total results (not pageified)
|
||||||
|
|
||||||
per_page: results to include per page
|
per_page: results to include per page
|
||||||
|
|
||||||
page: starting page
|
page: page requested
|
||||||
|
|
||||||
pages: pages to return starting from page
|
|
||||||
|
|
||||||
q: string to search for (fuzzy)
|
q: string to search for (fuzzy)
|
||||||
|
|
||||||
brick: brick to search for (absolute)
|
brick: brick to search for (absolute type, fuzzy string)
|
||||||
|
|
||||||
set: brick to search for (absolute)
|
set: brick to search for (absolute, fuzzy string)
|
||||||
|
|
||||||
### /api/special/
|
### /api/special/
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,67 @@
|
|||||||
|
const ControllerMaster = require('./controller-master.js');
|
||||||
const Database = require('../database/database.js');
|
const Database = require('../database/database.js');
|
||||||
|
|
||||||
const PgFormat = require('pg-format');
|
const PgFormat = require('pg-format');
|
||||||
|
|
||||||
|
async function Search(fuzzyString) {
|
||||||
|
await Database.Query('BEGIN TRANSACTION;');
|
||||||
|
const dbres = await Database.Query(`
|
||||||
|
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 ~* $1 OR lego_brick.name ~* $1 OR tag.name ~* $1
|
||||||
|
`, [fuzzyString]);
|
||||||
|
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',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// order by levenshtine distance
|
||||||
|
const bricks = dbres.rows;
|
||||||
|
bricks.sort((a, b) => {
|
||||||
|
const aName = a.name.toLowerCase();
|
||||||
|
const bName = b.name.toLowerCase();
|
||||||
|
const aTag = a.tag.toLowerCase();
|
||||||
|
const bTag = b.tag.toLowerCase();
|
||||||
|
const aFuzzy = fuzzyString.toLowerCase();
|
||||||
|
const bFuzzy = fuzzyString.toLowerCase();
|
||||||
|
|
||||||
|
const aDist = ControllerMaster.LevenshteinDistance(aName, aFuzzy);
|
||||||
|
const bDist = ControllerMaster.LevenshteinDistance(bName, bFuzzy);
|
||||||
|
const aTagDist = ControllerMaster.LevenshteinDistance(aTag, aFuzzy);
|
||||||
|
const bTagDist = ControllerMaster.LevenshteinDistance(bTag, bFuzzy);
|
||||||
|
|
||||||
|
if (aDist < bDist) {
|
||||||
|
return -1;
|
||||||
|
} else if (aDist > bDist) {
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
if (aTagDist < bTagDist) {
|
||||||
|
return -1;
|
||||||
|
} else if (aTagDist > bTagDist) {
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// combine tags into a single array
|
||||||
|
for (const brick of bricks) {
|
||||||
|
brick.type = 'brick';
|
||||||
|
brick.tags = brick.tag.split(',');
|
||||||
|
}
|
||||||
|
|
||||||
|
return bricks;
|
||||||
|
}
|
||||||
|
|
||||||
async function GetBulkBricks(bricksArr) {
|
async function GetBulkBricks(bricksArr) {
|
||||||
await Database.Query('BEGIN TRANSACTION;');
|
await Database.Query('BEGIN TRANSACTION;');
|
||||||
const dbres = await Database.Query(PgFormat(`
|
const dbres = await Database.Query(PgFormat(`
|
||||||
@@ -24,6 +85,7 @@ async function GetBulkBricks(bricksArr) {
|
|||||||
const bricks = dbres.rows;
|
const bricks = dbres.rows;
|
||||||
// combine tags into a single array
|
// combine tags into a single array
|
||||||
for (const brick of bricks) {
|
for (const brick of bricks) {
|
||||||
|
brick.type = 'brick';
|
||||||
brick.tags = brick.tag.split(',');
|
brick.tags = brick.tag.split(',');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,6 +140,7 @@ async function GetBrick(brickId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
Search,
|
||||||
GetBulkBricks,
|
GetBulkBricks,
|
||||||
GetBrick,
|
GetBrick,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,3 +1,56 @@
|
|||||||
|
|
||||||
|
// http://stackoverflow.com/questions/11919065/sort-an-array-by-the-levenshtein-distance-with-best-performance-in-javascript
|
||||||
|
function LevenshteinDistance(s, t) {
|
||||||
|
const d = []; // 2d matrix
|
||||||
|
|
||||||
|
// Step 1
|
||||||
|
const n = s.length;
|
||||||
|
const m = t.length;
|
||||||
|
|
||||||
|
if (n === 0) return m;
|
||||||
|
if (m === 0) return n;
|
||||||
|
|
||||||
|
// Create an array of arrays in javascript (a descending loop is quicker)
|
||||||
|
for (let i = n; i >= 0; i--) d[i] = [];
|
||||||
|
|
||||||
|
// Step 2
|
||||||
|
for (let i = n; i >= 0; i--) d[i][0] = i;
|
||||||
|
for (let j = m; j >= 0; j--) d[0][j] = j;
|
||||||
|
|
||||||
|
// Step 3
|
||||||
|
for (let i = 1; i <= n; i++) {
|
||||||
|
const si = s.charAt(i - 1);
|
||||||
|
|
||||||
|
// Step 4
|
||||||
|
for (let j = 1; j <= m; j++) {
|
||||||
|
// Check the jagged ld total so far
|
||||||
|
if (i === j && d[i][j] > 4) return n;
|
||||||
|
|
||||||
|
const tj = t.charAt(j - 1);
|
||||||
|
const cost = (si === tj) ? 0 : 1; // Step 5
|
||||||
|
|
||||||
|
// Calculate the minimum
|
||||||
|
let mi = d[i - 1][j] + 1;
|
||||||
|
const b = d[i][j - 1] + 1;
|
||||||
|
const c = d[i - 1][j - 1] + cost;
|
||||||
|
|
||||||
|
if (b < mi) mi = b;
|
||||||
|
if (c < mi) mi = c;
|
||||||
|
|
||||||
|
d[i][j] = mi; // Step 6
|
||||||
|
|
||||||
|
// Damerau transposition
|
||||||
|
if (i > 1 && j > 1 && si === t.charAt(j - 2) && s.charAt(i - 2) === tj) {
|
||||||
|
d[i][j] = Math.min(d[i][j], d[i - 2][j - 2] + cost);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 7
|
||||||
|
return d[n][m];
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
LevenshteinDistance,
|
||||||
ResultsPerPage: 16,
|
ResultsPerPage: 16,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,65 @@
|
|||||||
const ControllerMaster = require('./controller-master.js');
|
const ControllerMaster = require('./controller-master.js');
|
||||||
const Database = require('../database/database.js');
|
const Database = require('../database/database.js');
|
||||||
|
|
||||||
|
async function Search(fuzzyString) {
|
||||||
|
await Database.Query('BEGIN TRANSACTION;');
|
||||||
|
const dbres = await Database.Query(`
|
||||||
|
SELECT lego_set.id, lego_set.name, tag.name AS "tag", inv.price, inv.new_price AS "discount"
|
||||||
|
FROM lego_set
|
||||||
|
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 lego_set_inventory AS inv ON inv.set_id = lego_set.id
|
||||||
|
WHERE lego_set.id ~* $1 OR lego_set.name ~* $1 OR tag.name ~* $1
|
||||||
|
`, [fuzzyString]);
|
||||||
|
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',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// order by levenshtine distance
|
||||||
|
const sets = dbres.rows;
|
||||||
|
sets.sort((a, b) => {
|
||||||
|
const aName = a.name.toLowerCase();
|
||||||
|
const bName = b.name.toLowerCase();
|
||||||
|
const aTag = a.tag.toLowerCase();
|
||||||
|
const bTag = b.tag.toLowerCase();
|
||||||
|
const aFuzzy = fuzzyString.toLowerCase();
|
||||||
|
const bFuzzy = fuzzyString.toLowerCase();
|
||||||
|
|
||||||
|
const aDist = ControllerMaster.LevenshteinDistance(aName, aFuzzy);
|
||||||
|
const bDist = ControllerMaster.LevenshteinDistance(bName, bFuzzy);
|
||||||
|
const aTagDist = ControllerMaster.LevenshteinDistance(aTag, aFuzzy);
|
||||||
|
const bTagDist = ControllerMaster.LevenshteinDistance(bTag, bFuzzy);
|
||||||
|
|
||||||
|
if (aDist < bDist) {
|
||||||
|
return -1;
|
||||||
|
} else if (aDist > bDist) {
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
if (aTagDist < bTagDist) {
|
||||||
|
return -1;
|
||||||
|
} else if (aTagDist > bTagDist) {
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// combine tags into a single array
|
||||||
|
for (const set of sets) {
|
||||||
|
set.type = 'set';
|
||||||
|
set.tags = set.tag.split(',');
|
||||||
|
}
|
||||||
|
|
||||||
|
return sets;
|
||||||
|
}
|
||||||
|
|
||||||
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(`
|
||||||
@@ -78,6 +137,7 @@ async function GetSets(page, resPerPage) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
Search,
|
||||||
GetSet,
|
GetSet,
|
||||||
GetSets,
|
GetSets,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
const Models = require('./model-manager.js');
|
|
||||||
const { DataTypes, DataConstraints } = require('../database/database.js');
|
|
||||||
const { ORM } = Models.Database;
|
|
||||||
|
|
||||||
function Init() {
|
|
||||||
ORM.addModel('catagory', {
|
|
||||||
id: {
|
|
||||||
type: DataTypes.INTEGER,
|
|
||||||
constraints: [DataConstraints.PRIMARY_KEY, DataConstraints.NOT_NULL],
|
|
||||||
},
|
|
||||||
name: DataTypes.VARCHAR(100),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
Init,
|
|
||||||
};
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
const Models = require('./model-manager.js');
|
|
||||||
const { DataTypes, DataConstraints } = require('../database/database.js');
|
|
||||||
const { ORM } = Models.Database;
|
|
||||||
|
|
||||||
function Init() {
|
|
||||||
ORM.addModel('lego_brick', {
|
|
||||||
id: {
|
|
||||||
type: DataTypes.VARCHAR(50),
|
|
||||||
constraints: [DataConstraints.PRIMARY_KEY, DataConstraints.NOT_NULL],
|
|
||||||
},
|
|
||||||
catagory: {
|
|
||||||
type: DataTypes.INTEGER,
|
|
||||||
constraints: [DataConstraints.FOREIGN_KEY_REF(ORM.model('catagory').property('id'))],
|
|
||||||
},
|
|
||||||
date_released: DataTypes.TIMESTAMP,
|
|
||||||
dimenions_x: DataTypes.DECIMAL,
|
|
||||||
dimenions_y: DataTypes.DECIMAL,
|
|
||||||
dimenions_z: DataTypes.DECIMAL,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
Init,
|
|
||||||
};
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
const fs = require('fs');
|
|
||||||
|
|
||||||
function Init(databaseInstance) {
|
|
||||||
module.exports.Database = databaseInstance;
|
|
||||||
module.exports.Models = {};
|
|
||||||
|
|
||||||
const files = fs.readdirSync(__dirname).reverse();
|
|
||||||
files.forEach(file => {
|
|
||||||
if (file !== 'model-manager.js') {
|
|
||||||
const model = require(`./${file}`);
|
|
||||||
module.exports.Models[file.split('.')[0]] = model;
|
|
||||||
model.Init();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
Init,
|
|
||||||
};
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
// 15 days from now
|
// AppEng Deadline
|
||||||
const EndDate = new Date(1651269600 * 1000);
|
const EndDate = new Date(1651269600 * 1000);
|
||||||
|
|
||||||
function Special(req, res, next) {
|
function Special(req, res, next) {
|
||||||
|
|||||||
@@ -1,42 +1,102 @@
|
|||||||
|
const ControllerMaster = require('../controllers/controller-master.js');
|
||||||
|
const BrickController = require('../controllers/brick-controller.js');
|
||||||
|
const SetController = require('../controllers/set-controller.js');
|
||||||
|
|
||||||
async function Search(req, res) {
|
async function Search(req, res) {
|
||||||
const q = req.query.q;
|
const q = req.query.q;
|
||||||
console.log(q);
|
|
||||||
|
const pageRequested = req.query.page || 1;
|
||||||
|
const perPage = req.query.per_page || 16;
|
||||||
|
|
||||||
|
// TODO: it is tricky to do a database offset / limit here
|
||||||
|
// due to the fact that we have to combine the results of
|
||||||
|
// the two queries, look into me (maybe merging the queries)
|
||||||
|
const brickResults = await BrickController.Search(q);
|
||||||
|
const setResults = await SetController.Search(q);
|
||||||
|
|
||||||
|
if (brickResults.error && setResults.error) {
|
||||||
|
return res.send(JSON.stringify({
|
||||||
|
error: 'Not found',
|
||||||
|
long: 'What you are looking for do not exist',
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
let count = 0;
|
||||||
|
if (brickResults) count += brickResults.length;
|
||||||
|
if (setResults) count += setResults.length;
|
||||||
|
|
||||||
|
if (brickResults.error) {
|
||||||
|
// remove after the requested page
|
||||||
|
setResults.splice(perPage * pageRequested);
|
||||||
|
// remove before the requested page
|
||||||
|
setResults.splice(0, perPage * (pageRequested - 1));
|
||||||
|
return res.send(JSON.stringify({
|
||||||
|
data: setResults,
|
||||||
|
page: {
|
||||||
|
total: count,
|
||||||
|
per_page: perPage,
|
||||||
|
page: pageRequested,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (setResults.error) {
|
||||||
|
// remove after the requested page
|
||||||
|
brickResults.splice(perPage * pageRequested);
|
||||||
|
// remove before the requested page
|
||||||
|
brickResults.splice(0, perPage * (pageRequested - 1));
|
||||||
|
return res.send(JSON.stringify({
|
||||||
|
data: brickResults,
|
||||||
|
page: {
|
||||||
|
total: count,
|
||||||
|
per_page: perPage,
|
||||||
|
page: pageRequested,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// organise into the most relevant 10 results
|
||||||
|
const results = [...brickResults, ...setResults];
|
||||||
|
results.sort((a, b) => {
|
||||||
|
const aName = a.name.toLowerCase();
|
||||||
|
const bName = b.name.toLowerCase();
|
||||||
|
const aTag = a.tag.toLowerCase();
|
||||||
|
const bTag = b.tag.toLowerCase();
|
||||||
|
const aFuzzy = q.toLowerCase();
|
||||||
|
const bFuzzy = q.toLowerCase();
|
||||||
|
|
||||||
|
const aDist = ControllerMaster.LevenshteinDistance(aName, aFuzzy);
|
||||||
|
const bDist = ControllerMaster.LevenshteinDistance(bName, bFuzzy);
|
||||||
|
const aTagDist = ControllerMaster.LevenshteinDistance(aTag, aFuzzy);
|
||||||
|
const bTagDist = ControllerMaster.LevenshteinDistance(bTag, bFuzzy);
|
||||||
|
|
||||||
|
if (aDist < bDist) {
|
||||||
|
return -1;
|
||||||
|
} else if (aDist > bDist) {
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
if (aTagDist < bTagDist) {
|
||||||
|
return -1;
|
||||||
|
} else if (aTagDist > bTagDist) {
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// remove after the requested page
|
||||||
|
results.splice(perPage * pageRequested);
|
||||||
|
// remove before the requested page
|
||||||
|
results.splice(0, perPage * (pageRequested - 1));
|
||||||
|
|
||||||
res.send(JSON.stringify({
|
res.send(JSON.stringify({
|
||||||
data: [
|
data: results,
|
||||||
{
|
page: {
|
||||||
id: '1010',
|
total: count,
|
||||||
type: 'set',
|
per_page: perPage,
|
||||||
name: q,
|
page: pageRequested,
|
||||||
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',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,10 +28,9 @@ async function Featured(req, res) {
|
|||||||
res.send(JSON.stringify({
|
res.send(JSON.stringify({
|
||||||
data: [...sets],
|
data: [...sets],
|
||||||
page: {
|
page: {
|
||||||
total_sent: sets.length,
|
total: sets.length,
|
||||||
per_page: 8,
|
per_page: 8,
|
||||||
current_page: 1,
|
page: 1,
|
||||||
last_page: 1,
|
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user