Compare commits

...

3 Commits

Author SHA1 Message Date
73a4f4e595 chore: convert everything to vue sfc 2018-09-08 17:39:46 -07:00
421ec9f0db chore: add and configure poi 2018-09-08 16:25:05 -07:00
49ef9a6155 chore: fix linting script 2018-09-08 16:21:20 -07:00
15 changed files with 4306 additions and 591 deletions

View File

@@ -6,7 +6,7 @@
"module": "index.mjs",
"private": true,
"scripts": {
"lint": "eslint \"*.{js,mjs}\" \"src/**/*.{js,mjs}\"",
"lint": "eslint \"packages/**/*.{js,mjs,vue}\" \"packages/**/src/**/*.{js,mjs,vue}\"",
"precommit": "lint-staged",
"version": "auto-changelog -p && auto-authors && git add CHANGELOG.md AUTHORS.md",
"start": "node .",

View File

@@ -0,0 +1,23 @@
{
"extends": [
"../../.eslintrc",
"plugin:vue/essential"
],
"parserOptions": {
"parser": "babel-eslint",
"ecmaVersion": 8,
"sourceType": "module"
},
"plugins": [
"vue"
],
"rules": {
"import/no-extraneous-dependencies": [
"error", {
"devDependencies": ["packages/search-site/poi.config.js"],
"optionalDependencies": false,
"peerDependencies": false
}
]
}
}

View File

View File

@@ -6,10 +6,9 @@
"module": "index.mjs",
"description": "strain search static website",
"scripts": {
"start": "node . serve",
"build": "node . build",
"dev": "nodemon -i dist/ -w src -e mjs,ejs -x 'node . build'",
"deploy": "node . build && firebase deploy"
"start": "poi",
"build": "poi build",
"deploy": "npm run build && firebase deploy"
},
"author": "joe fleming (https://github.com/w33ble)",
"license": "MIT",
@@ -18,10 +17,19 @@
},
"dependencies": {
"ejs": "^2.6.1",
"esm": "^3.0.81"
"esm": "^3.0.81",
"lunr": "^2.3.3",
"mitt": "^1.1.3",
"vue": "^2.5.17",
"vue-router": "^3.0.1",
"vue-template-compiler": "^2.5.17"
},
"devDependencies": {
"@poi/plugin-vue-static": "^1.0.7",
"babel-eslint": "^9.0.0",
"eslint-plugin-vue": "^4.7.1",
"firebase-tools": "^4.2.1",
"nodemon": "^1.18.4"
"nodemon": "^1.18.4",
"poi": "^10.2.10"
}
}

View File

@@ -0,0 +1,8 @@
/* eslint global-require: 0 */
const path = require('path');
module.exports = {
entry: path.resolve(__dirname, 'src/index.mjs'),
outDir: 'dist',
plugins: [require('@poi/plugin-vue-static')()],
};

View File

@@ -0,0 +1,138 @@
<template>
<form @submit.prevent="handleSubmit">
<div class="container">
<h1 class="title">
Strain Search
</h1>
<!-- Name Search Input -->
<div class="field">
<label class="label">Search By Name</label>
<div class="control">
<input ref="name" class="input" type="text" placeholder="Text input">
<p v-if="error.length" class="help is-danger">{{error}}</p>
</div>
</div>
<!-- Multi-Selects -->
<div class="columns filters">
<div class="column is-half">
<div class="columns is-mobile">
<div class="column is-half">
<div class="field">
<label class="label">Desired Effects</label>
<div class="select is-multiple">
<select ref="effects" multiple size="6">
<option v-for="effect in effects" :key="effect" :value="effect">{{effect | capitalize}}</option>
</select>
</div>
</div>
</div>
<div class="column is-half">
<div class="field">
<label class="label">Medical Use</label>
<div class="select is-multiple">
<select ref="uses" multiple size="6">
<option v-for="use in uses" :key="use" :value="use">{{use | capitalize}}</option>
</select>
</div>
</div>
</div>
</div>
</div>
<div class="column is-half">
<div class="columns is-mobile">
<div class="column is-half">
<div class="field">
<label class="label">Condition</label>
<div class="select is-multiple">
<select ref="conditions" multiple size="6">
<option v-for="condition in conditions" :key="condition" :value="condition">{{condition | capitalize}}</option>
</select>
</div>
</div>
</div>
<div class="column is-half">
<div class="field">
<label class="label">Flavor</label>
<div class="select is-multiple">
<select ref="flavors" multiple size="6">
<option v-for="flavor in flavors" :key="flavor" :value="flavor">{{flavor | capitalize}}</option>
</select>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Form Submits -->
<div class="field is-grouped">
<div class="control">
<button type="submit" class="button is-link">Submit</button>
</div>
<div class="control">
<button type="button" class="button is-text" @click.prevent="resetForm">Clear</button>
</div>
</div>
</div>
</form>
</template>
<script>
import emitter from '../lib/emitter.mjs';
const getMultiValues = node => Array.from(node.selectedOptions).map(o => o.value);
export default {
props: {
effects: Array,
uses: Array,
conditions: Array,
flavors: Array,
},
data() {
return {
error: '',
};
},
created() {
emitter.on('error', this.setError);
},
beforeDestroy() {
emitter.off('error', this.setError);
},
methods: {
handleSubmit() {
const requirements = {
name: this.$refs.name.value,
effects: getMultiValues(this.$refs.effects),
uses: getMultiValues(this.$refs.uses),
conditions: getMultiValues(this.$refs.conditions),
flavors: getMultiValues(this.$refs.flavors),
};
this.error = '';
emitter.emit('search', requirements);
},
resetForm() {
this.$el.reset();
},
setError(msg) {
this.error = msg;
},
},
};
</script>
<style scoped>
.filters .select {
display: block;
}
.filters .select select[multiple] {
width: 100%;
}
</style>

View File

@@ -0,0 +1,75 @@
<template>
<div class="card">
<header class="card-header">
<p class="card-header-title">{{strain.name}}</p>
<div class="tags" style="margin: 0 12px; padding: 6px 0;">
<span v-if="strain.category === 'indica'" class="tag is-rounded is-indica">{{strain.category}}</span>
<span v-if="strain.category === 'sativa'" class="tag is-rounded is-sativa">{{strain.category}}</span>
<span v-if="strain.category === 'hybrid'" class="tag is-rounded is-hybrid">{{strain.category}}</span>
<span class="tag is-rounded is-light">{{strain.rating | round}} ({{strain.rating_count}})</span>
</div>
</header>
<div class="card-content">
<TagList title="Effects" :name="strain.name" :tags="strain.effects" />
<TagList title="Uses" :name="strain.name" :tags="strain.uses" />
<TagList title="Conditions" :name="strain.name" :tags="strain.conditions" />
<TagList title="Flavors" :name="strain.name" :tags="strain.flavors" />
</div>
<footer class="card-footer">
<div class="card-footer-item">
<button
class="button"
:class="{ 'is-success': favorite }"
@click.prevent="toggleFavorite(strain.id)"
>
Save
</button>
</div>
</footer>
</div>
</template>
<script>
import store from '../lib/store.mjs';
import emitter from '../lib/emitter.mjs';
import TagList from './TagList.vue';
export default {
components: {
TagList,
},
props: {
strain: Object,
},
data() {
const favs = store.get('favorites') || [];
return {
favorite: favs.indexOf(this.strain.id) >= 0,
};
},
methods: {
toggleFavorite() {
this.favorite = !this.favorite;
emitter.emit('favorite', { id: this.strain.id, isFav: this.favorite });
},
},
};
</script>
<style scoped>
.tag.is-indica {
background-color: hsl(217, 71%, 53%);
color: #fff;
}
.tag.is-sativa {
background-color: hsl(348, 100%, 61%);
color: #fff;
}
.tag.is-hybrid {
background-color: hsl(271, 100%, 71%);
color: #fff;
}
</style>

View File

@@ -0,0 +1,62 @@
<template>
<div class="container">
<div>
<h3 v-if="strains.length === 0" class="title is-3">No Matching Strains :(</h3>
<h3 v-if="strains.length > 0" class="title is-3">Found {{strains.length}} Strains</h3>
</div>
<div class="cards">
<StrainCard v-for="strain in strains" :key="strain.id" :strain="strain" />
</div>
</div>
</template>
<script>
import StrainCard from './StrainCard.vue';
export default {
components: {
StrainCard,
},
props: {
strains: {
type: Array,
required: true,
},
},
};
</script>
<style scoped>
.cards {
display: flex;
flex-wrap: wrap;
}
.cards .card {
width: 100%;
margin: 12px;
}
.cards .card .tag-list--title {
font-size: 0.8em;
}
@media screen and (min-width: 769px) {
.cards .card {
width: 45%;
}
}
@media screen and (min-width: 1280px) {
.cards .card {
width: 30%;
}
}
@media screen and (max-width: 860px) {
.cards .card .tag-list--title {
font-size: 0.7em;
}
}
</style>

View File

@@ -0,0 +1,22 @@
<template>
<div v-if="tags.length" class="tag-list columns is-mobile">
<div class="tag-list--title column is-one-quarter">
{{title}}
</div>
<div class="tag-list--tag column">
<div v-if="tags.length" class="tags">
<span v-for="tag in tags" :key="name + tag" class="tag is-rounded">{{tag}}</span>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
tags: Array,
title: String,
name: String,
},
};
</script>

View File

@@ -1,417 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.1/css/bulma.min.css">
<title>Strain Search</title>
<style>
.tag:not(body).is-indica {
background-color: hsl(217, 71%, 53%);
color: #fff;
}
.tag:not(body).is-sativa {
background-color: hsl(348, 100%, 61%);
color: #fff;
}
.tag:not(body).is-hybrid {
background-color: hsl(271, 100%, 71%);
color: #fff;
}
#strain-list .cards {
display: flex;
flex-wrap: wrap;
}
#strain-list .cards .card {
width: 100%;
margin: 12px;
}
#strain-list .cards .card .tag-list--title {
font-size: 0.8em;
}
@media screen and (min-width: 769px) {
#strain-list .cards .card {
width: 45%;
}
}
@media screen and (min-width: 1280px) {
#strain-list .cards .card {
width: 30%;
}
}
@media screen and (max-width: 860px) {
#strain-list .cards .card .tag-list--title {
font-size: 0.7em;
}
}
#search-form .filters .select {
display: block;
}
#search-form .filters .select select[multiple] {
width: 100%;
}
</style>
</head>
<body>
<noscript><h1>You're going to want to enable JavaScript</h1></noscript>
<section class="section">
<form id="search-form" @submit.prevent="handleSubmit">
<div class="container">
<h1 class="title">
Strain Search
</h1>
<!-- Name Search Input -->
<div class="field">
<label class="label">Search By Name</label>
<div class="control">
<input ref="name" class="input" type="text" placeholder="Text input">
<p v-if="error.length" class="help is-danger">{{error}}</p>
</div>
</div>
<!-- Multi-Selects -->
<div class="columns filters">
<div class="column is-half">
<div class="columns is-mobile">
<div class="column is-half">
<div class="field">
<label class="label">Desired Effects</label>
<div class="select is-multiple">
<select ref="effects" multiple size="6">
<option v-for="effect in effects" :value="effect">{{effect | capitalize}}</option>
</select>
</div>
</div>
</div>
<div class="column is-half">
<div class="field">
<label class="label">Medical Use</label>
<div class="select is-multiple">
<select ref="uses" multiple size="6">
<option v-for="use in uses" :value="use">{{use | capitalize}}</option>
</select>
</div>
</div>
</div>
</div>
</div>
<div class="column is-half">
<div class="columns is-mobile">
<div class="column is-half">
<div class="field">
<label class="label">Condition</label>
<div class="select is-multiple">
<select ref="conditions" multiple size="6">
<option v-for="condition in conditions" :value="condition">{{condition | capitalize}}</option>
</select>
</div>
</div>
</div>
<div class="column is-half">
<div class="field">
<label class="label">Flavor</label>
<div class="select is-multiple">
<select ref="flavors" multiple size="6">
<option v-for="flavor in flavors" :value="flavor">{{flavor | capitalize}}</option>
</select>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Form Submits -->
<div class="field is-grouped">
<div class="control">
<button type="submit" class="button is-link">Submit</button>
</div>
<div class="control">
<button type="button" class="button is-text" @click.prevent="resetForm">Clear</button>
</div>
</div>
</div>
</form>
</section>
<section class="section" id="strain-list">
<div v-if="strains.length === 0" class="container">
<p>NO MATCHING STRAINS :(</p>
</div>
<div v-if="strains.length > 0" class="container">
<div>
<h3 class="title is-3">Found {{strains.length}} Strains</h3>
</div>
<div class="cards">
<strain-card v-for="strain in strains" :key="strain.id" :strain="strain" />
</div>
</div>
</section>
<!-- dev build of vue, useful for debugging -->
<!-- <script src="https://unpkg.com/vue@2.5.17/dist/vue.js"></script> -->
<!-- production build of vue, with template compiler -->
<script src="https://unpkg.com/vue@2.5.17/dist/vue.min.js"></script>
<script src="https://unpkg.com/lunr@2.3.1/lunr.js"></script>
<script src="https://unpkg.com/mitt@1.1.3/dist/mitt.umd.js"></script>
<script>
// lunr = window.lunr
// mitt = window.mitt
(function ({ mitt, lunr, localStorage }, data) {
const emitter = mitt();
const stringAscending = (a, b) => {
const aa = a.toLowerCase();
const bb = b.toLowerCase();
if (aa === bb) return 0;
return aa < bb ? -1 : 1;
}
// helpers
const getMultiValues = node => Array.from(node.selectedOptions).map(o => o.value);
const store = {
get: id => JSON.parse(localStorage.getItem(id)),
set: (id, val) => localStorage.setItem(id, JSON.stringify(val)),
};
// listeners
emitter.on('favorite', ({ id, isFav }) => {
const favs = store.get('favorites') || [];
const idx = favs.indexOf(id);
// remove previously favorited strain
if (idx >= 0 && !isFav) {
store.set('favorites', favs.filter(f => f !== id));
} else if (idx === -1 && isFav) {
store.set('favorites', favs.concat(id));
}
});
// vue helpers
Vue.filter('capitalize', value => {
if (!value) return '';
const v = value.toString();
return v.charAt(0).toUpperCase() + v.slice(1);
});
Vue.filter('round', (value, digits = 2) => {
const v = parseFloat(value, 10);
return Math.round(v * (10 * digits)) / (10 * digits);
});
Vue.component('tag-list', {
props: {
tags: Array,
title: String,
},
template: `
<div v-if="tags.length" class="tag-list columns is-mobile">
<div class="tag-list--title column is-one-quarter">
{{title}}
</div>
<div class="tag-list--tag column">
<div v-if="tags.length" class="tags">
<span v-for="tag in tags" class="tag is-rounded">{{tag}}</span>
</div>
</div>
</div>
`,
});
Vue.component('strain-card', {
data() {
const favs = store.get('favorites') || [];
return {
favorite: favs.indexOf(this.strain.id) >= 0,
};
},
props: {
strain: Object,
},
methods: {
toggleFavorite(id) {
this.favorite = !this.favorite;
emitter.emit('favorite', { id: this.strain.id, isFav: this.favorite });
}
},
template: `
<div class="card">
<header class="card-header">
<p class="card-header-title">{{strain.name}}</p>
<div class="tags" style="margin: 0 12px; padding: 6px 0;">
<span v-if="strain.category === 'indica'" class="tag is-rounded is-indica">{{strain.category}}</span>
<span v-if="strain.category === 'sativa'" class="tag is-rounded is-sativa">{{strain.category}}</span>
<span v-if="strain.category === 'hybrid'" class="tag is-rounded is-hybrid">{{strain.category}}</span>
<span class="tag is-rounded is-light">{{strain.rating | round}} ({{strain.rating_count}})</span>
</div>
</header>
<div class="card-content">
<tag-list title="Effects" :tags="strain.effects" />
<tag-list title="Uses" :tags="strain.uses" />
<tag-list title="Conditions" :tags="strain.conditions" />
<tag-list title="Flavors" :tags="strain.flavors" />
</div>
<footer class="card-footer">
<div class="card-footer-item">
<button class="button" :class="{ 'is-success': favorite }" @click.prevent="toggleFavorite(strain.id)">Save</button>
</div>
</footer>
</div>
`,
});
// form handler
new Vue({
el: '#search-form',
data() {
return {
effects: data.effects.sort(stringAscending),
uses: data.uses.sort(stringAscending),
conditions: data.conditions.sort(stringAscending),
flavors: data.flavors.sort(stringAscending),
error: '',
};
},
created() {
emitter.on('error', this.setError);
},
beforeDestroy() {
emitter.off('error', this.setError);
},
methods: {
handleSubmit() {
const requirements = {
name: this.$refs.name.value,
effects: getMultiValues(this.$refs.effects),
uses: getMultiValues(this.$refs.uses),
conditions: getMultiValues(this.$refs.conditions),
flavors: getMultiValues(this.$refs.flavors),
}
this.error = '';
emitter.emit('search', requirements);
},
resetForm() {
this.$el.reset();
},
setError(msg) {
this.error = msg
},
},
});
new Vue({
el: '#strain-list',
data() {
return {
all_strains: data.strains,
strains: [],
requirements: {
name: '',
effects: [],
uses: [],
conditions: [],
flavors: [],
},
};
},
created() {
// lunr setup
this.idx = window.idx = lunr(function () {
this.ref('id');
this.field('name');
this.field('effects');
this.field('uses');
this.field('conditions');
this.field('flavors');
data.strains.forEach(doc => this.add(doc));
});
this.searchListener = reqs => {
this.requirements = reqs;
this.updateStrains();
}
emitter.on('search', r => this.searchListener(r));
this.updateStrains();
},
beforeDestroy() {
emitter.off('search', r => this.searchListener(r));
},
methods: {
updateStrains(limit = 40) {
const hasName = this.requirements.name.length > 0;
const hasFilters =
this.requirements.effects.length > 0 ||
this.requirements.uses.length > 0 ||
this.requirements.conditions.length > 0 ||
this.requirements.flavors.length > 0;
if (!hasName && !hasFilters) {
this.strains = this
.all_strains
.slice(0, limit);
return;
}
const searchParts = [this.requirements.name];
this.requirements.effects.forEach(t => searchParts.push(`+effects:${t}`));
this.requirements.uses.forEach(t => searchParts.push(`+uses:${t}`));
this.requirements.conditions.forEach(t => searchParts.push(`+conditions:${t}`));
this.requirements.flavors.forEach(t => searchParts.push(`+flavors:${t}`));
try {
const hits = this.idx
.search(searchParts.join(' '))
// .slice(0, limit);
const refs = hits.map(({ ref }) => parseInt(ref, 10));
this.strains = this
.all_strains
.map((strain, i) => {
const idx = refs.indexOf(strain.id);
if (idx < 0) return;
return Object.assign({ score: hits[idx].score }, strain)
})
.filter(Boolean)
.sort((a, b) => {
if (a.score === b.score) {
// sort matching search score by adjusted rating
if (a.rating_adjusted === b.rating_adjusted) return 0;
return a.rating_adjusted > b.rating_adjusted ? -1 : 1;
}
return a.score > b.score ? -1 : 1;
});
} catch(err) {
this.strains = [];
emitter.emit('error', err.message);
}
},
},
});
})(this, <%- data %>);
</script>
</body>
</html>

View File

@@ -1,105 +1,33 @@
import http from 'http';
import fs from 'fs';
import ejs from 'ejs';
import Vue from 'vue';
import Router from 'vue-router';
const srcFile = 'src/index.ejs';
const destFile = 'dist/index.html';
Vue.use(Router);
function getData() {
return new Promise((resolve, reject) => {
fs.readFile('../scraper/db.json', (err, str) => {
if (err) reject(err);
else resolve(JSON.parse(str));
});
});
}
async function build() {
const data = await getData();
const options = {};
// calculate adjusted ratings
const totalMean =
data.strains.reduce((acc, strain) => acc + strain.rating + strain.rating_count, 0) /
data.strains.length;
data.strains = data.strains.map(strain => {
const minRatings = 10;
const { rating, rating_count: count } = strain;
return Object.assign(strain, {
rating_adjusted:
(((count / (count + minRatings)) * rating) / (minRatings / (count + minRatings))) *
totalMean,
});
Vue.filter('capitalize', value => {
if (!value) return '';
const v = value.toString();
return v.charAt(0).toUpperCase() + v.slice(1);
});
// order strains by rating
data.strains = data.strains.sort((n, strain) => {
if (strain.rating_adjusted === n.rating_adjusted) return 0;
return strain.rating_adjusted < n.rating_adjusted ? -1 : 1;
Vue.filter('round', (value, digits = 2) => {
const v = parseFloat(value, 10);
return Math.round(v * (10 * digits)) / (10 * digits);
});
return new Promise((resolve, reject) => {
ejs.renderFile(srcFile, { data: JSON.stringify(data) }, options, (err, str) => {
if (err) reject(err);
else {
fs.writeFile(destFile, str, er => {
if (er) reject(er);
else {
console.log(`Site built: ${destFile}`);
resolve();
}
const router = new Router({
mode: 'history',
routes: [
{
path: '/',
name: 'home',
component: () => import(/* webpackChunkName: "homeapp" */ './pages/Home.vue'),
},
],
});
}
const app = new Vue({
router,
render: h => h('div', { attrs: { id: 'app' } }, [h('router-view')]),
});
});
}
async function serve() {
const PORT = '3000';
await build();
http
.createServer((req, res) => {
fs.readFile(destFile, (err, str) => {
if (err) {
res.writeHead(500, { 'Content-Type': 'text/plain' });
res.end('Failed :(');
console.error(err);
return;
}
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(str);
});
})
.listen(PORT, () => {
console.log(`Server listening on http://localhost:${PORT}`);
});
}
export default async function() {
const cmds = ['build', 'serve'];
const cmd = process.argv.splice(2)[0];
try {
switch (cmd) {
case 'build': {
await build();
break;
}
case 'serve': {
await serve();
break;
}
default: {
const msg = `Please use one of ${cmds.map(c => `"${c}"`).join(', ')}`;
if (cmd.length) console.error(`Unknown command "${cmd}". ${msg}`);
else console.error(`No command provided. ${msg}`);
}
}
} catch (err) {
console.error(err);
}
}
export default app;

View File

@@ -0,0 +1,5 @@
import mitt from 'mitt';
const emitter = mitt();
export default emitter;

View File

@@ -0,0 +1,17 @@
/* eslint-env browser */
const storage = (() => {
// return localstorage in the browser env
if (this && this.localStorage) return this.localStorage;
// return a mock localstorage in the server env
return {
getItem: () => null,
setItem: () => null,
};
})();
export default {
get: id => JSON.parse(storage.getItem(id)),
set: (id, val) => storage.setItem(id, JSON.stringify(val)),
};

View File

@@ -0,0 +1,149 @@
<template>
<div>
<section class="section">
<SearchForm :effects="effects" :uses="uses" :conditions="conditions" :flavors="flavors" />
</section>
<section class="section">
<StrainList :strains="matches" />
</section>
</div>
</template>
<script>
import lunr from 'lunr';
import SearchForm from '../components/SearchForm.vue';
import StrainList from '../components/StrainList.vue';
import data from '../../../scraper/db.json';
import emitter from '../lib/emitter.mjs';
import store from '../lib/store.mjs';
export default {
name: 'Home',
components: {
SearchForm,
StrainList,
},
head: {
title: 'Strain Search',
link: [
{
rel: 'stylesheet',
href: 'https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.1/css/bulma.min.css',
},
],
},
data() {
return {
...data,
matches: [],
requirements: {
name: '',
effects: [],
uses: [],
conditions: [],
flavors: [],
},
};
},
computed: {
hasName() {
return this.requirements.name.length > 0;
},
hasFilters() {
return (
this.requirements.effects.length > 0 ||
this.requirements.uses.length > 0 ||
this.requirements.conditions.length > 0 ||
this.requirements.flavors.length > 0
);
},
searchParams() {
const searchParts = [this.requirements.name];
this.requirements.effects.forEach(t => searchParts.push(`+effects:${t}`));
this.requirements.uses.forEach(t => searchParts.push(`+uses:${t}`));
this.requirements.conditions.forEach(t => searchParts.push(`+conditions:${t}`));
this.requirements.flavors.forEach(t => searchParts.push(`+flavors:${t}`));
return searchParts.join(' ');
},
},
methods: {
showDefaults(limit = 40) {
// const favs = store.get('favorites') || [];
// const favStrains = this.strains.filter(({ id }) => favs.indexOf(id) !== -1);
// this.matches = favStrains.concat(this.strains.slice(0, limit - favStrains.length));
this.matches = this.strains.slice(0, limit);
},
updateMatches(limit = 40) {
if (!this.hasName && !this.hasFilters) {
this.showDefaults(limit);
return;
}
try {
const hits = this.idx.search(this.searchParams);
// .slice(0, limit);
const refs = hits.map(({ ref }) => parseInt(ref, 10));
this.matches = this.strains
.map(strain => {
const idx = refs.indexOf(strain.id);
if (idx < 0) return null;
return Object.assign({ score: hits[idx].score }, strain);
})
.filter(Boolean)
.sort((a, b) => {
if (a.score === b.score) {
// sort matching search score by adjusted rating
if (a.rating_adjusted === b.rating_adjusted) return 0;
return a.rating_adjusted > b.rating_adjusted ? -1 : 1;
}
return a.score > b.score ? -1 : 1;
});
} catch (err) {
this.matches = [];
emitter.emit('error', err.message);
}
},
},
created() {
// lunr setup
this.idx = lunr(function lunrSetup() {
this.ref('id');
this.field('name');
this.field('effects');
this.field('uses');
this.field('conditions');
this.field('flavors');
data.strains.forEach(doc => this.add(doc));
});
// function to handle search form submissions
this.searchListener = reqs => {
this.requirements = reqs;
this.updateMatches();
};
// listen for search form submissions
emitter.on('search', r => this.searchListener(r));
// listen for favorite changes
emitter.on('favorite', ({ id, isFav }) => {
const favs = store.get('favorites') || [];
const idx = favs.indexOf(id);
// remove previously favorited strain
if (idx >= 0 && !isFav) {
store.set('favorites', favs.filter(f => f !== id));
} else if (idx === -1 && isFav) {
store.set('favorites', favs.concat(id));
}
});
this.updateMatches(); // set initial match list
},
beforeDestroy() {
emitter.off('search', r => this.searchListener(r));
},
};
</script>

3833
yarn.lock

File diff suppressed because it is too large Load Diff