Compare commits
29 Commits
4591a47dba
...
v1.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 6949aa7780 | |||
| 2b9e76e475 | |||
| b4ce9557f3 | |||
| 3ab623fea1 | |||
| 1f2e307f05 | |||
| b73957e7df | |||
| 2c60ec4afc | |||
| 8c45a533b0 | |||
| 3154b1e014 | |||
| 85efbd84d1 | |||
| 3b39345fa6 | |||
| 1c2df7733d | |||
| 8780245986 | |||
| 90f3fba45a | |||
| b1d244e34c | |||
| 01871f083b | |||
| 2020e76b3e | |||
| d5b0cd0730 | |||
| 5bdd35b74b | |||
| fa2db8d430 | |||
| a2bea32fb6 | |||
| bebd51b047 | |||
| 4539515bf7 | |||
| 81d41f74b4 | |||
| 7d70224bdf | |||
| 9c4641dd0a | |||
| 5e47f71f31 | |||
| 0414e1aae6 | |||
| e3e95e7c2b |
3
AUTHORS.md
Normal file
3
AUTHORS.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
### Authors
|
||||||
|
|
||||||
|
- joe fleming ([w33ble](https://github.com/w33ble))
|
||||||
29
CHANGELOG.md
Normal file
29
CHANGELOG.md
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
### Changelog
|
||||||
|
|
||||||
|
#### [v1.1.0](https://git.w33ble.com/w33ble/strain-tools/compare/v1.0.2...v1.1.0) (18 September 2018)
|
||||||
|
- feat: basic routing and fav nav [`3b39345`](https://git.w33ble.com/w33ble/strain-tools/commit/3b39345fa66ac4748a400e9e80e032ec9ae9697b)
|
||||||
|
- feat: show favorite strain cards [`85efbd8`](https://git.w33ble.com/w33ble/strain-tools/commit/85efbd84d1704c55a3eefae7fa2d36e7fd7f3cc2)
|
||||||
|
- fix: weird bug in StrainList [`2c60ec4`](https://git.w33ble.com/w33ble/strain-tools/commit/2c60ec4afc16f81fd6b610443091d26d278d3f81)
|
||||||
|
|
||||||
|
#### [v1.0.2](https://git.w33ble.com/w33ble/strain-tools/compare/v1.0.1...v1.0.2) (13 September 2018)
|
||||||
|
|
||||||
|
#### [v1.0.1](https://git.w33ble.com/w33ble/strain-tools/compare/v1.0.0...v1.0.1) (13 September 2018)
|
||||||
|
- fix: correct localstorage fallback [`d5b0cd0`](https://git.w33ble.com/w33ble/strain-tools/commit/d5b0cd07307dbf06d8d4530803a00c2b2ab8c4c5)
|
||||||
|
- docs: update readme with usable scripts [`5bdd35b`](https://git.w33ble.com/w33ble/strain-tools/commit/5bdd35b74b417706c76bd50d7162567b60dbdcb2)
|
||||||
|
- fix: scrape strains alphabetically [`fa2db8d`](https://git.w33ble.com/w33ble/strain-tools/commit/fa2db8d4304b0b0d91221b4ef942daa74ab3df94)
|
||||||
|
|
||||||
|
#### v1.0.0 (9 September 2018)
|
||||||
|
- fix: assign key to strain cards [`e3e95e7`](https://git.w33ble.com/w33ble/strain-tools/commit/e3e95e7c2b390ca52633e84604cf6a4a9b239d54)
|
||||||
|
- fix: remove listener on form unmount [`4591a47`](https://git.w33ble.com/w33ble/strain-tools/commit/4591a47dbaa62cbd330bbc9b96ace2be1ace8bd6)
|
||||||
|
- feat: add fav/unfav control [`50a3b1d`](https://git.w33ble.com/w33ble/strain-tools/commit/50a3b1db92a64fcd7e10e35ba045a3970a1440ba)
|
||||||
|
- feat: adjust card tag label by screen size [`5665bde`](https://git.w33ble.com/w33ble/strain-tools/commit/5665bdeaa99bcbf4fc5a9d13d98803b17a42404b)
|
||||||
|
- feat: sort by adjusted rating [`712c5c0`](https://git.w33ble.com/w33ble/strain-tools/commit/712c5c095c52269d7ffbd7f763cdf325b11369a4)
|
||||||
|
- feat: show errors, sort tags [`943c091`](https://git.w33ble.com/w33ble/strain-tools/commit/943c0917c71db1612aefd9f2fc8bde098000364c)
|
||||||
|
- feat: functional search on site [`d3b2110`](https://git.w33ble.com/w33ble/strain-tools/commit/d3b21103eb5227101f581fcc853714bc78ea9950)
|
||||||
|
- feat: even more site functionality [`9a1f2f9`](https://git.w33ble.com/w33ble/strain-tools/commit/9a1f2f9785e36bd2de2a3a1fbf46bbc9cfdb90d3)
|
||||||
|
- feat: add tags to scraper output [`804fac1`](https://git.w33ble.com/w33ble/strain-tools/commit/804fac1afb68dc52f482cbfa393856b14b64400a)
|
||||||
|
- feat: mocked up search form on site [`22a95c3`](https://git.w33ble.com/w33ble/strain-tools/commit/22a95c3fe827ec0651c44164f58950dcc283354b)
|
||||||
|
- feat: simple site build and dev server [`699a03c`](https://git.w33ble.com/w33ble/strain-tools/commit/699a03c7c06da011b64bdc358287f5d653c96c92)
|
||||||
|
- feat: add a search site package [`37dcabd`](https://git.w33ble.com/w33ble/strain-tools/commit/37dcabde5e20a6ecea803f40798ae43c72e3afe8)
|
||||||
|
- feat: working scraper [`cc5e15d`](https://git.w33ble.com/w33ble/strain-tools/commit/cc5e15dbb11f388ba260da6c196fe6827e947d3c)
|
||||||
|
- initial commit [`c81b30b`](https://git.w33ble.com/w33ble/strain-tools/commit/c81b30be896009b48e66005007f88e55fb159c9c)
|
||||||
12
package.json
12
package.json
@@ -1,14 +1,16 @@
|
|||||||
{
|
{
|
||||||
"name": "strain-tools",
|
"name": "strain-tools",
|
||||||
"version": "0.0.0",
|
"version": "1.1.0",
|
||||||
"description": "strain tools",
|
"description": "strain tools",
|
||||||
"main": "index",
|
"main": "index",
|
||||||
"module": "index.mjs",
|
"module": "index.mjs",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "eslint \"*.{js,mjs}\" \"src/**/*.{js,mjs}\"",
|
"lint": "eslint \"packages/*/*.{js,mjs,vue}\" \"packages/*/src/**/*.{js,mjs,vue}\"",
|
||||||
"precommit": "lint-staged",
|
"precommit": "lint-staged",
|
||||||
"version": "auto-changelog -p && auto-authors && git add CHANGELOG.md AUTHORS.md",
|
"prepush": "npm run lint",
|
||||||
|
"sync-versions": "node -r esm scripts/version-sync.mjs",
|
||||||
|
"version": "npm run sync-versions && auto-changelog -p && auto-authors && git add CHANGELOG.md AUTHORS.md packages",
|
||||||
"start": "node .",
|
"start": "node .",
|
||||||
"dev": "nodemon --ignore db.json ."
|
"dev": "nodemon --ignore db.json ."
|
||||||
},
|
},
|
||||||
@@ -31,10 +33,10 @@
|
|||||||
"commitLimit": false
|
"commitLimit": false
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*.{js,mjs}": [
|
"*.{js,mjs,vue}": [
|
||||||
"eslint --fix"
|
"eslint --fix"
|
||||||
],
|
],
|
||||||
"*.{js,mjs,json,css}": [
|
"*.{js,mjs,vue,json,css}": [
|
||||||
"prettier --write"
|
"prettier --write"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,5 +8,3 @@ Clone repo and run the command. Resulting data can be found in `db.json`.
|
|||||||
yarn install
|
yarn install
|
||||||
yarn start
|
yarn start
|
||||||
```
|
```
|
||||||
|
|
||||||
**NOTE**: You may need to run it multiple times (4 or 5 should do it), since some strains will get skipped the first few times. I don't know if it's leafly's endpoint or some weird race condition in the scraping code though.
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "leafly-scraper",
|
"name": "scraper",
|
||||||
"version": "0.0.0",
|
"version": "1.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "scrapes strain info, stores for later reference",
|
"description": "scrapes strain info, stores for later reference",
|
||||||
"main": "index",
|
"main": "index",
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.18.0",
|
"axios": "^0.18.0",
|
||||||
"esm": "^3.0.81",
|
"esm": "^3.0.82",
|
||||||
"lodash": "^4.17.10",
|
"lodash": "^4.17.10",
|
||||||
"lowdb": "^1.0.0"
|
"lowdb": "^1.0.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,14 +8,14 @@ const adapter = new FileAsync('db.json');
|
|||||||
const xhr = axios.create({
|
const xhr = axios.create({
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/json, text/plain, */*',
|
Accept: 'application/json, text/plain, */*',
|
||||||
Referer: 'https://www.leafly.com/explore',
|
Referer: 'https://www.leafly.com/explore/sort-alpha',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const pSeries = tasks => tasks.reduce((c, task) => c.then(task), Promise.resolve());
|
const pSeries = tasks => tasks.reduce((c, task) => c.then(task), Promise.resolve());
|
||||||
|
|
||||||
const getPage = async num => {
|
const getPage = async num => {
|
||||||
const url = `https://www.leafly.com/explore/page-${num}`;
|
const url = `https://www.leafly.com/explore/page-${num}/sort-alpha`;
|
||||||
const response = await xhr.get(url, {
|
const response = await xhr.get(url, {
|
||||||
responseType: 'json',
|
responseType: 'json',
|
||||||
});
|
});
|
||||||
|
|||||||
23
packages/search-site/.eslintrc
Normal file
23
packages/search-site/.eslintrc
Normal 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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,12 @@ Strain search static website. Use it to search for strains by name, effects, med
|
|||||||
|
|
||||||
Based on data from leafly. You'll need to run the scraper first since that's where the data comes from.
|
Based on data from leafly. You'll need to run the scraper first since that's where the data comes from.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
- `yarn start`: starts the dev server, with HMR
|
||||||
|
- `yarn build`: builds the static site into the `dist` path
|
||||||
|
- `yarn deploy`: builds and deploys the app onto firebase
|
||||||
|
|
||||||
#### License
|
#### License
|
||||||
|
|
||||||
MIT © [w33ble](https://github.com/w33ble)
|
MIT © [w33ble](https://github.com/w33ble)
|
||||||
|
|||||||
0
packages/search-site/dist/.empty
vendored
0
packages/search-site/dist/.empty
vendored
@@ -1,15 +1,14 @@
|
|||||||
{
|
{
|
||||||
"name": "search-site",
|
"name": "search-site",
|
||||||
"version": "0.0.0",
|
"version": "1.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"main": "index",
|
"main": "index",
|
||||||
"module": "index.mjs",
|
"module": "index.mjs",
|
||||||
"description": "strain search static website",
|
"description": "strain search static website",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node . serve",
|
"start": "poi",
|
||||||
"build": "node . build",
|
"build": "poi build",
|
||||||
"dev": "nodemon -i dist/ -w src -e mjs,ejs -x 'node . build'",
|
"deploy": "npm run build && firebase deploy"
|
||||||
"deploy": "node . build && firebase deploy"
|
|
||||||
},
|
},
|
||||||
"author": "joe fleming (https://github.com/w33ble)",
|
"author": "joe fleming (https://github.com/w33ble)",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -17,11 +16,17 @@
|
|||||||
"cjs": true
|
"cjs": true
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ejs": "^2.6.1",
|
"esm": "^3.0.82",
|
||||||
"esm": "^3.0.81"
|
"lunr": "^2.3.3",
|
||||||
|
"vue": "^2.5.17",
|
||||||
|
"vue-router": "^3.0.1",
|
||||||
|
"vue-template-compiler": "^2.5.17"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@poi/plugin-vue-static": "^1.0.7",
|
||||||
|
"babel-eslint": "^9.0.0",
|
||||||
|
"eslint-plugin-vue": "^4.7.1",
|
||||||
"firebase-tools": "^4.2.1",
|
"firebase-tools": "^4.2.1",
|
||||||
"nodemon": "^1.18.4"
|
"poi": "^10.2.10"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
12
packages/search-site/poi.config.js
Normal file
12
packages/search-site/poi.config.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
/* 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')({
|
||||||
|
routes: ['/', '/favorites'],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
};
|
||||||
14
packages/search-site/src/components/AppNav.vue
Normal file
14
packages/search-site/src/components/AppNav.vue
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<template>
|
||||||
|
<div id="app-nav" class="tabs">
|
||||||
|
<ul>
|
||||||
|
<li class="is-active"><a>Search</a></li>
|
||||||
|
<li><a>Favorites</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'AppNav',
|
||||||
|
};
|
||||||
|
</script>
|
||||||
119
packages/search-site/src/components/SearchForm.vue
Normal file
119
packages/search-site/src/components/SearchForm.vue
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
<template>
|
||||||
|
<form @submit.prevent="handleSubmit">
|
||||||
|
<!-- 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>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const getMultiValues = node => Array.from(node.selectedOptions).map(o => o.value);
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
effects: Array,
|
||||||
|
uses: Array,
|
||||||
|
conditions: Array,
|
||||||
|
flavors: Array,
|
||||||
|
error: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
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.$emit('search', requirements);
|
||||||
|
},
|
||||||
|
resetForm() {
|
||||||
|
this.$el.reset();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.filters .select {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters .select select[multiple] {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
88
packages/search-site/src/components/StrainCard.vue
Normal file
88
packages/search-site/src/components/StrainCard.vue
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
<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 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() {
|
||||||
|
const isFav = !this.favorite;
|
||||||
|
const { id } = this.strain;
|
||||||
|
const favs = store.get('favorites') || [];
|
||||||
|
const idx = favs.indexOf(id);
|
||||||
|
|
||||||
|
// update component state
|
||||||
|
this.favorite = isFav;
|
||||||
|
|
||||||
|
// update the store
|
||||||
|
if (idx >= 0 && !isFav) {
|
||||||
|
// remove previously favorited strain
|
||||||
|
store.set('favorites', favs.filter(f => f !== id));
|
||||||
|
} else if (idx === -1 && isFav) {
|
||||||
|
// add new favorited strain
|
||||||
|
store.set('favorites', favs.concat(id));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</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>
|
||||||
62
packages/search-site/src/components/StrainList.vue
Normal file
62
packages/search-site/src/components/StrainList.vue
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<h3 v-show="strains.length === 0" class="title is-3">No Matching Strains :(</h3>
|
||||||
|
<h3 v-show="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>
|
||||||
22
packages/search-site/src/components/TagList.vue
Normal file
22
packages/search-site/src/components/TagList.vue
Normal 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>
|
||||||
12
packages/search-site/src/filters.mjs
Normal file
12
packages/search-site/src/filters.mjs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import Vue from 'vue';
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
@@ -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" :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>
|
|
||||||
@@ -1,105 +1,51 @@
|
|||||||
import http from 'http';
|
import Vue from 'vue';
|
||||||
import fs from 'fs';
|
import Router from 'vue-router';
|
||||||
import ejs from 'ejs';
|
import './filters.mjs';
|
||||||
|
import strainData from './plugins/strainData.mjs';
|
||||||
|
import lunr from './plugins/lunr.mjs';
|
||||||
|
|
||||||
const srcFile = 'src/index.ejs';
|
Vue.use(strainData);
|
||||||
const destFile = 'dist/index.html';
|
|
||||||
|
|
||||||
function getData() {
|
Vue.use(
|
||||||
return new Promise((resolve, reject) => {
|
lunr(function lunrSetup() {
|
||||||
fs.readFile('../scraper/db.json', (err, str) => {
|
// lunr search index setup
|
||||||
if (err) reject(err);
|
this.ref('id');
|
||||||
else resolve(JSON.parse(str));
|
this.field('name');
|
||||||
});
|
this.field('effects');
|
||||||
});
|
this.field('uses');
|
||||||
}
|
this.field('conditions');
|
||||||
|
this.field('flavors');
|
||||||
async function build() {
|
Vue.prototype.$strainData.strains.forEach(doc => this.add(doc));
|
||||||
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,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
});
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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() {
|
Vue.use(Router);
|
||||||
const cmds = ['build', 'serve'];
|
|
||||||
const cmd = process.argv.splice(2)[0];
|
|
||||||
|
|
||||||
try {
|
const router = new Router({
|
||||||
switch (cmd) {
|
mode: 'history',
|
||||||
case 'build': {
|
routes: [
|
||||||
await build();
|
{
|
||||||
break;
|
path: '/',
|
||||||
}
|
component: () => import(/* webpackChunkName: "Home" */ './pages/Home.vue'),
|
||||||
case 'serve': {
|
children: [
|
||||||
await serve();
|
{
|
||||||
break;
|
path: '',
|
||||||
}
|
name: 'search',
|
||||||
default: {
|
component: () => import(/* webpackChunkName: "Search" */ './pages/Search.vue'),
|
||||||
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}`);
|
path: 'favorites',
|
||||||
}
|
name: 'favorites',
|
||||||
}
|
component: () => import(/* webpackChunkName: "Favorites" */ './pages/Favorites.vue'),
|
||||||
} catch (err) {
|
},
|
||||||
console.error(err);
|
],
|
||||||
}
|
},
|
||||||
}
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const app = new Vue({
|
||||||
|
router,
|
||||||
|
render: h => h('div', { attrs: { id: 'app' } }, [h('router-view')]),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default app;
|
||||||
|
|||||||
18
packages/search-site/src/lib/store.mjs
Normal file
18
packages/search-site/src/lib/store.mjs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
/* eslint-env browser */
|
||||||
|
|
||||||
|
const storage = (() => {
|
||||||
|
try {
|
||||||
|
return window.localStorage;
|
||||||
|
} catch (err) {
|
||||||
|
// 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)),
|
||||||
|
};
|
||||||
43
packages/search-site/src/pages/Favorites.vue
Normal file
43
packages/search-site/src/pages/Favorites.vue
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<template>
|
||||||
|
<div id="favorites-page">
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="title">
|
||||||
|
Favorites
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<h3 class="title is-5">
|
||||||
|
Strains you have saved
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<div class="container">
|
||||||
|
<StrainList :strains="favoriteStrains" no-save-control />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import store from '../lib/store.mjs';
|
||||||
|
import StrainList from '../components/StrainList.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'FavoritesPage',
|
||||||
|
components: {
|
||||||
|
StrainList,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
const favorites = store.get('favorites') || [];
|
||||||
|
|
||||||
|
return {
|
||||||
|
favorites,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
favoriteStrains() {
|
||||||
|
return this.$strainData.strains.filter(strain => this.favorites.indexOf(strain.id) !== -1);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
40
packages/search-site/src/pages/Home.vue
Normal file
40
packages/search-site/src/pages/Home.vue
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<template>
|
||||||
|
<div id="home-page">
|
||||||
|
<div id="app-nav" class="tabs is-centered">
|
||||||
|
<ul>
|
||||||
|
<li :class="{'is-active': activeTab === 'search'}" @click="setActive('search')">
|
||||||
|
<router-link :to="{name: 'search'}">Search</router-link>
|
||||||
|
</li>
|
||||||
|
<li :class="{'is-active': activeTab === 'favorites'}" @click="setActive('favorites')">
|
||||||
|
<router-link :to="{name: 'favorites'}">Favorites</router-link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<router-view></router-view>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'HomePage',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
activeTab: this.$route.name,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
setActive(name) {
|
||||||
|
this.activeTab = name;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
head: {
|
||||||
|
title: 'Strain Search',
|
||||||
|
link: [
|
||||||
|
{
|
||||||
|
rel: 'stylesheet',
|
||||||
|
href: 'https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.1/css/bulma.min.css',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
140
packages/search-site/src/pages/Search.vue
Normal file
140
packages/search-site/src/pages/Search.vue
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
<template>
|
||||||
|
<div id="search-page">
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="title">
|
||||||
|
Strain Search
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<div class="container">
|
||||||
|
<SearchForm
|
||||||
|
:effects="effects"
|
||||||
|
:uses="uses"
|
||||||
|
:conditions="conditions"
|
||||||
|
:flavors="flavors"
|
||||||
|
:error="error"
|
||||||
|
@search="onSearch"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<div class="container">
|
||||||
|
<StrainList :strains="matches" />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import SearchForm from '../components/SearchForm.vue';
|
||||||
|
import StrainList from '../components/StrainList.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'SearchPage',
|
||||||
|
components: {
|
||||||
|
SearchForm,
|
||||||
|
StrainList,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
error: '',
|
||||||
|
matches: [],
|
||||||
|
requirements: {
|
||||||
|
name: '',
|
||||||
|
effects: [],
|
||||||
|
uses: [],
|
||||||
|
conditions: [],
|
||||||
|
flavors: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
strains() {
|
||||||
|
return this.$strainData.strains;
|
||||||
|
},
|
||||||
|
effects() {
|
||||||
|
return this.$strainData.effects;
|
||||||
|
},
|
||||||
|
uses() {
|
||||||
|
return this.$strainData.uses;
|
||||||
|
},
|
||||||
|
conditions() {
|
||||||
|
return this.$strainData.conditions;
|
||||||
|
},
|
||||||
|
flavors() {
|
||||||
|
return this.$strainData.flavors;
|
||||||
|
},
|
||||||
|
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: {
|
||||||
|
onSearch(reqs) {
|
||||||
|
this.requirements = reqs;
|
||||||
|
this.updateMatches();
|
||||||
|
},
|
||||||
|
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) {
|
||||||
|
this.error = '';
|
||||||
|
|
||||||
|
if (!this.hasName && !this.hasFilters) {
|
||||||
|
this.showDefaults(limit);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const hits = this.$lunr.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 = [];
|
||||||
|
this.error = err.message;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.updateMatches(); // set initial match list
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
10
packages/search-site/src/plugins/lunr.mjs
Normal file
10
packages/search-site/src/plugins/lunr.mjs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
/* eslint no-param-reassign: 0 */
|
||||||
|
import lunr from 'lunr';
|
||||||
|
|
||||||
|
export default function(lunrSetup) {
|
||||||
|
return {
|
||||||
|
install(Vue) {
|
||||||
|
Vue.prototype.$lunr = lunr(lunrSetup);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
8
packages/search-site/src/plugins/strainData.mjs
Normal file
8
packages/search-site/src/plugins/strainData.mjs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/* eslint no-param-reassign: 0 */
|
||||||
|
import data from '../../../scraper/db.json';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
install(Vue) {
|
||||||
|
Vue.prototype.$strainData = data;
|
||||||
|
},
|
||||||
|
};
|
||||||
26
scripts/version-sync.mjs
Normal file
26
scripts/version-sync.mjs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import fs from 'fs';
|
||||||
|
import { promisify } from 'util';
|
||||||
|
import { join } from 'path';
|
||||||
|
import pkg from '../package.json';
|
||||||
|
|
||||||
|
const readDir = promisify(fs.readdir);
|
||||||
|
const readFile = promisify(fs.readFile);
|
||||||
|
const writeFile = promisify(fs.writeFile);
|
||||||
|
|
||||||
|
async function syncPackageVersions() {
|
||||||
|
const packagesPath = 'packages'; // path for all packages
|
||||||
|
const { version } = pkg;
|
||||||
|
|
||||||
|
const packages = await readDir(packagesPath);
|
||||||
|
|
||||||
|
packages.forEach(async pack => {
|
||||||
|
const packagePath = join(packagesPath, pack, 'package.json');
|
||||||
|
const p = JSON.parse(await readFile(packagePath, 'utf-8'));
|
||||||
|
p.version = version;
|
||||||
|
await writeFile(packagePath, `${JSON.stringify(p, null, 2)}\n`);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Versions with root: ${version}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
syncPackageVersions();
|
||||||
Reference in New Issue
Block a user