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);
|
||||
}
|
||||
|
||||
async OnMount() {
|
||||
const route = this.state.getroute;
|
||||
const products = await fetch(route).then(response => response.json());
|
||||
async FetchListings(from) {
|
||||
const products = await fetch(from).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.getState,
|
||||
products,
|
||||
current_page: products.page.current_page,
|
||||
last_page: products.page.last_page,
|
||||
products: newProducts,
|
||||
page: products.page.page,
|
||||
per_page: products.page.per_page,
|
||||
total: products.page.total,
|
||||
});
|
||||
}
|
||||
|
||||
Render() {
|
||||
OnMount() {
|
||||
this.loading = 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.loadingBar = '';
|
||||
} else {
|
||||
@@ -39,7 +49,7 @@ class ProductList extends Component {
|
||||
template: /* html */`
|
||||
<h2>{this.state.title}</h2>
|
||||
<div class="product-list">
|
||||
${this.state.products.data.map(product => {
|
||||
${this.state.products.map(product => {
|
||||
return `<compact-listing-component name="${product.name}"
|
||||
id="${product.id}"
|
||||
listing="${product.listing}"
|
||||
@@ -151,13 +161,31 @@ class ProductList extends Component {
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
OnRender() {
|
||||
this.loading = false;
|
||||
// scroll to bottom event listener
|
||||
if (this.keepLoading) {
|
||||
window.addEventListener('scroll', () => {
|
||||
if (window.innerHeight + window.scrollY >= document.body.offsetHeight) {
|
||||
console.log('scrolled to bottom');
|
||||
window.addEventListener('scroll', async () => {
|
||||
// start loading 200px before the bottom of the page
|
||||
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;
|
||||
flex-wrap: wrap;
|
||||
font-size: 0.6em;
|
||||
position: fixed;
|
||||
width: 65%;
|
||||
position: absolute;
|
||||
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 {
|
||||
@@ -104,15 +112,28 @@ class Search extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
OnRender() {
|
||||
const searchBar = this.root.querySelector('#search-bar');
|
||||
searchBar.value = localStorage.getItem('search-bar');
|
||||
const route = `/api/search?q=${searchBar.value}&per_page=10`;
|
||||
GetSearch(value) {
|
||||
if (value === '') {
|
||||
this.AppendSearchResults([], true);
|
||||
return;
|
||||
}
|
||||
|
||||
const route = `/api/search?q=${value}&per_page=10`;
|
||||
fetch(route).then((response) => {
|
||||
return response.json();
|
||||
}).then((data) => {
|
||||
if (data.error) {
|
||||
this.AppendSearchResults([], true);
|
||||
return;
|
||||
}
|
||||
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) => {
|
||||
localStorage.setItem('search-bar', e.target.value);
|
||||
@@ -123,15 +144,10 @@ class Search extends Component {
|
||||
}
|
||||
|
||||
// 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);
|
||||
});
|
||||
this.GetSearch(e.target.value);
|
||||
|
||||
if (e.keyCode === 13) {
|
||||
const searchTerm = searchBar.value;
|
||||
const searchTerm = e.target.value;
|
||||
if (searchTerm.length > 0) {
|
||||
window.location.href = `/search?q=${searchTerm}`;
|
||||
}
|
||||
|
||||
@@ -15,21 +15,21 @@ class StoreFront extends Component {
|
||||
<img class="carousel-image" src="/res/lego-image1.jpg" alt="">
|
||||
<div class="carousel-caption">
|
||||
<h1>Welcome to LegoLog!</h1>
|
||||
<button>Shop LEGO® Now</button>
|
||||
<a href="/featured/"><button>Shop LEGO® Now</button></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="carousel-cell">
|
||||
<img class="carousel-image" src="res/warehouse.png" alt="">
|
||||
<div class="carousel-caption">
|
||||
<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 class="carousel-cell">
|
||||
<img class="carousel-image" src="res/space.png" alt="">
|
||||
<div class="carousel-caption">
|
||||
<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 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>
|
||||
<limited-margin>
|
||||
<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>
|
||||
</body>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>LegoLog Home!</title>
|
||||
<title>LegoLog!</title>
|
||||
<meta name="viewport">
|
||||
<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 -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
@@ -34,19 +34,5 @@
|
||||
<navbar-component></navbar-component>
|
||||
<limited-margin>
|
||||
<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>
|
||||
</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
|
||||
|
||||
total: total results (not pageified)
|
||||
|
||||
per_page: results to include per page
|
||||
|
||||
page: starting page
|
||||
|
||||
pages: pages to return starting from page
|
||||
page: page requested
|
||||
|
||||
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/
|
||||
|
||||
|
||||
@@ -1,6 +1,67 @@
|
||||
const ControllerMaster = require('./controller-master.js');
|
||||
const Database = require('../database/database.js');
|
||||
|
||||
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) {
|
||||
await Database.Query('BEGIN TRANSACTION;');
|
||||
const dbres = await Database.Query(PgFormat(`
|
||||
@@ -24,6 +85,7 @@ async function GetBulkBricks(bricksArr) {
|
||||
const bricks = dbres.rows;
|
||||
// combine tags into a single array
|
||||
for (const brick of bricks) {
|
||||
brick.type = 'brick';
|
||||
brick.tags = brick.tag.split(',');
|
||||
}
|
||||
|
||||
@@ -78,6 +140,7 @@ async function GetBrick(brickId) {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
Search,
|
||||
GetBulkBricks,
|
||||
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 = {
|
||||
LevenshteinDistance,
|
||||
ResultsPerPage: 16,
|
||||
};
|
||||
|
||||
@@ -1,6 +1,65 @@
|
||||
const ControllerMaster = require('./controller-master.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) {
|
||||
await Database.Query('BEGIN TRANSACTION;');
|
||||
const dbres = await Database.Query(`
|
||||
@@ -78,6 +137,7 @@ async function GetSets(page, resPerPage) {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
Search,
|
||||
GetSet,
|
||||
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);
|
||||
|
||||
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) {
|
||||
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({
|
||||
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',
|
||||
},
|
||||
],
|
||||
data: results,
|
||||
page: {
|
||||
total: count,
|
||||
per_page: perPage,
|
||||
page: pageRequested,
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -28,10 +28,9 @@ async function Featured(req, res) {
|
||||
res.send(JSON.stringify({
|
||||
data: [...sets],
|
||||
page: {
|
||||
total_sent: sets.length,
|
||||
total: sets.length,
|
||||
per_page: 8,
|
||||
current_page: 1,
|
||||
last_page: 1,
|
||||
page: 1,
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user