diff --git a/client/public/about/index.html b/client/public/about/index.html
new file mode 100644
index 0000000..0ec73a7
--- /dev/null
+++ b/client/public/about/index.html
@@ -0,0 +1,42 @@
+
+
+
diff --git a/client/public/featured/index.html b/client/public/featured/index.html
new file mode 100644
index 0000000..3b53e9a
--- /dev/null
+++ b/client/public/featured/index.html
@@ -0,0 +1,46 @@
+
+
+
LegoLog: Featured Sets
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client/public/index.html b/client/public/index.html
index 3963b71..5815c6d 100644
--- a/client/public/index.html
+++ b/client/public/index.html
@@ -38,20 +38,5 @@
-
-
-
-
-
diff --git a/client/public/product/index.html b/client/public/product/index.html
index 59ca8fa..3866364 100644
--- a/client/public/product/index.html
+++ b/client/public/product/index.html
@@ -1,9 +1,9 @@
-
LegoLog Home!
+
LegoLog!
-
+
@@ -34,19 +34,5 @@
-
-
-
-
diff --git a/client/public/search/index.html b/client/public/search/index.html
new file mode 100644
index 0000000..1f1745f
--- /dev/null
+++ b/client/public/search/index.html
@@ -0,0 +1,54 @@
+
+
+
LegoLog: Featured Sets
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/API.md b/docs/API.md
index c56be6d..ea69a55 100644
--- a/docs/API.md
+++ b/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/
diff --git a/src/controllers/brick-controller.js b/src/controllers/brick-controller.js
index abdd67a..1c2ca3f 100644
--- a/src/controllers/brick-controller.js
+++ b/src/controllers/brick-controller.js
@@ -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,
};
diff --git a/src/controllers/controller-master.js b/src/controllers/controller-master.js
index e7f2025..df6ecc0 100644
--- a/src/controllers/controller-master.js
+++ b/src/controllers/controller-master.js
@@ -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,
};
diff --git a/src/controllers/set-controller.js b/src/controllers/set-controller.js
index e1c5e75..aa365c6 100644
--- a/src/controllers/set-controller.js
+++ b/src/controllers/set-controller.js
@@ -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,
};
diff --git a/src/models/catagory.js b/src/models/catagory.js
deleted file mode 100644
index d141123..0000000
--- a/src/models/catagory.js
+++ /dev/null
@@ -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,
-};
diff --git a/src/models/lego-brick.js b/src/models/lego-brick.js
deleted file mode 100644
index 4220c86..0000000
--- a/src/models/lego-brick.js
+++ /dev/null
@@ -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,
-};
diff --git a/src/models/model-manager.js b/src/models/model-manager.js
deleted file mode 100644
index 5aba5a6..0000000
--- a/src/models/model-manager.js
+++ /dev/null
@@ -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,
-};
diff --git a/src/routes/helpers.js b/src/routes/helpers.js
index 6fe3a8a..63dc2ea 100644
--- a/src/routes/helpers.js
+++ b/src/routes/helpers.js
@@ -1,4 +1,4 @@
-// 15 days from now
+// AppEng Deadline
const EndDate = new Date(1651269600 * 1000);
function Special(req, res, next) {
diff --git a/src/routes/query-router.js b/src/routes/query-router.js
index 91bc469..d671233 100644
--- a/src/routes/query-router.js
+++ b/src/routes/query-router.js
@@ -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,
+ },
}));
}
diff --git a/src/routes/sets-router.js b/src/routes/sets-router.js
index 98c90d9..4ba7015 100644
--- a/src/routes/sets-router.js
+++ b/src/routes/sets-router.js
@@ -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,
},
}));
}