Compare commits
30 Commits
9684309e6a
...
v1.0.1
| Author | SHA1 | Date | |
|---|---|---|---|
| 01871f083b | |||
| 2020e76b3e | |||
| d5b0cd0730 | |||
| 5bdd35b74b | |||
| fa2db8d430 | |||
| a2bea32fb6 | |||
| bebd51b047 | |||
| 4539515bf7 | |||
| 81d41f74b4 | |||
| 7d70224bdf | |||
| 9c4641dd0a | |||
| 5e47f71f31 | |||
| 0414e1aae6 | |||
| e3e95e7c2b | |||
| 4591a47dba | |||
| 50a3b1db92 | |||
| c652eee096 | |||
| a15deb9602 | |||
| 5665bdeaa9 | |||
| 712c5c095c | |||
| fe38c452b5 | |||
| 943c0917c7 | |||
| d3b21103eb | |||
| 9a1f2f9785 | |||
| fc158da8a1 | |||
| 804fac1afb | |||
| 22a95c3fe8 | |||
| 699a03c7c0 | |||
| 37dcabde5e | |||
| b9e5762213 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -7,6 +7,8 @@ yarn-error.log
|
||||
.env
|
||||
coverage
|
||||
.nyc_output
|
||||
.firebase
|
||||
.firebaserc
|
||||
coverage.lcov
|
||||
/lib
|
||||
dist/
|
||||
db.json
|
||||
3
AUTHORS.md
Normal file
3
AUTHORS.md
Normal file
@@ -0,0 +1,3 @@
|
||||
### Authors
|
||||
|
||||
- joe fleming ([w33ble](https://github.com/w33ble))
|
||||
17
CHANGELOG.md
Normal file
17
CHANGELOG.md
Normal file
@@ -0,0 +1,17 @@
|
||||
### Changelog
|
||||
|
||||
#### 1.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)
|
||||
@@ -1,10 +1,8 @@
|
||||
# strain-scraper
|
||||
# strain-tools
|
||||
|
||||
scrapes strain info, stores for later reference.
|
||||

|
||||
|
||||
[](https://raw.githubusercontent.com/w33ble/strain-scraper/master/LICENSE)
|
||||
[](https://www.npmjs.com/package/strain-scraper)
|
||||
[](https://nodejs.org/api/documentation.html#documentation_stability_index)
|
||||
Monorepo for cannabis strain tools. For repos, check in `packages`.
|
||||
|
||||
#### License
|
||||
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
{
|
||||
"name": "strain-tools",
|
||||
"version": "0.0.0",
|
||||
"version": "1.0.1",
|
||||
"description": "strain tools",
|
||||
"main": "index",
|
||||
"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",
|
||||
"prepush": "npm run lint",
|
||||
"version": "auto-changelog -p && auto-authors && git add CHANGELOG.md AUTHORS.md",
|
||||
"start": "node .",
|
||||
"dev": "nodemon --ignore db.json ."
|
||||
"dev": "nodemon --ignore db.json .",
|
||||
"setversion": "npx oao reset-all-versions"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
10
packages/scraper/README.md
Normal file
10
packages/scraper/README.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# leafly-scraper
|
||||
|
||||
Scrapes strain info, stores for later reference.
|
||||
|
||||
Clone repo and run the command. Resulting data can be found in `db.json`.
|
||||
|
||||
```
|
||||
yarn install
|
||||
yarn start
|
||||
```
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "leafly-scraper",
|
||||
"version": "0.0.0",
|
||||
"name": "scraper",
|
||||
"version": "1.0.1",
|
||||
"private": true,
|
||||
"description": "scrapes strain info, stores for later reference",
|
||||
"main": "index",
|
||||
"module": "index.mjs",
|
||||
@@ -18,14 +19,6 @@
|
||||
],
|
||||
"author": "joe fleming (https://github.com/w33ble)",
|
||||
"license": "MIT",
|
||||
"lint-staged": {
|
||||
"*.{js,mjs}": [
|
||||
"eslint --fix"
|
||||
],
|
||||
"*.{js,mjs,json,css}": [
|
||||
"prettier --write"
|
||||
]
|
||||
},
|
||||
"prettier": {
|
||||
"printWidth": 100,
|
||||
"singleQuote": true,
|
||||
@@ -36,7 +29,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.18.0",
|
||||
"esm": "^3.0.17",
|
||||
"esm": "^3.0.82",
|
||||
"lodash": "^4.17.10",
|
||||
"lowdb": "^1.0.0"
|
||||
},
|
||||
|
||||
@@ -8,12 +8,14 @@ const adapter = new FileAsync('db.json');
|
||||
const xhr = axios.create({
|
||||
headers: {
|
||||
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 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, {
|
||||
responseType: 'json',
|
||||
});
|
||||
@@ -45,28 +47,63 @@ export default async function scrapeLeafly(startFrom = 1, endAt = Infinity) {
|
||||
let finished = false;
|
||||
const db = await low(adapter);
|
||||
|
||||
await db.defaults({ strains: [] }).write();
|
||||
async function writeTag(type, tag) {
|
||||
const res = await db
|
||||
.get(type)
|
||||
.indexOf(tag)
|
||||
.value();
|
||||
|
||||
if (res < 0) {
|
||||
await db
|
||||
.get(type)
|
||||
.push(tag)
|
||||
.write();
|
||||
}
|
||||
}
|
||||
|
||||
async function writeTags(type, tags) {
|
||||
await pSeries(tags.map(tag => () => writeTag(type, tag)));
|
||||
}
|
||||
|
||||
async function writeDoc(strain) {
|
||||
// check for value
|
||||
const doc = db
|
||||
.get('strains')
|
||||
.filter({ id: strain.id })
|
||||
.first()
|
||||
.value();
|
||||
|
||||
if (!doc) {
|
||||
console.log(`Adding ${strain.id}, ${strain.name}`);
|
||||
await db
|
||||
.get('strains')
|
||||
.push(strain)
|
||||
.write();
|
||||
|
||||
await writeTags('effects', strain.effects);
|
||||
await writeTags('negative_effects', strain.negative_effects);
|
||||
await writeTags('uses', strain.uses);
|
||||
await writeTags('conditions', strain.conditions);
|
||||
await writeTags('flavors', strain.flavors);
|
||||
}
|
||||
}
|
||||
|
||||
await db
|
||||
.defaults({
|
||||
strains: [],
|
||||
effects: [],
|
||||
negative_effects: [],
|
||||
uses: [],
|
||||
conditions: [],
|
||||
flavors: [],
|
||||
})
|
||||
.write();
|
||||
|
||||
while (!finished) {
|
||||
console.log(`Fetching page ${pageNum}`);
|
||||
const data = await getPage(pageNum);
|
||||
|
||||
data.strains.forEach(async strain => {
|
||||
// check for value
|
||||
const doc = db
|
||||
.get('strains')
|
||||
.filter({ id: strain.id })
|
||||
.first()
|
||||
.value();
|
||||
|
||||
if (!doc) {
|
||||
console.log(`Adding ${strain.id}, ${strain.name}`);
|
||||
await db
|
||||
.get('strains')
|
||||
.push(strain)
|
||||
.write();
|
||||
}
|
||||
});
|
||||
await pSeries(data.strains.map(strain => () => writeDoc(strain)));
|
||||
|
||||
if (pageNum >= endAt || !data.strains.length || data.page.isLastPage) finished = true;
|
||||
pageNum += 1;
|
||||
|
||||
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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
17
packages/search-site/README.md
Normal file
17
packages/search-site/README.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# search-site
|
||||
|
||||

|
||||
|
||||
Strain search static website. Use it to search for strains by name, effects, medical uses, and other tags.
|
||||
|
||||
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
|
||||
|
||||
MIT © [w33ble](https://github.com/w33ble)
|
||||
12
packages/search-site/firebase.json
Normal file
12
packages/search-site/firebase.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"hosting": {
|
||||
"public": "dist",
|
||||
"ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
|
||||
"rewrites": [
|
||||
{
|
||||
"source": "**",
|
||||
"destination": "/index.html"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
5
packages/search-site/index.js
Normal file
5
packages/search-site/index.js
Normal file
@@ -0,0 +1,5 @@
|
||||
/* eslint no-global-assign: 0 */
|
||||
require = require('esm')(module);
|
||||
const mod = require('./src/index.mjs').default;
|
||||
|
||||
mod();
|
||||
3
packages/search-site/index.mjs
Normal file
3
packages/search-site/index.mjs
Normal file
@@ -0,0 +1,3 @@
|
||||
import mod from './src/index.mjs';
|
||||
|
||||
mod();
|
||||
35
packages/search-site/package.json
Normal file
35
packages/search-site/package.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "search-site",
|
||||
"version": "1.0.1",
|
||||
"private": true,
|
||||
"main": "index",
|
||||
"module": "index.mjs",
|
||||
"description": "strain search static website",
|
||||
"scripts": {
|
||||
"start": "poi",
|
||||
"build": "poi build",
|
||||
"deploy": "npm run build && firebase deploy"
|
||||
},
|
||||
"author": "joe fleming (https://github.com/w33ble)",
|
||||
"license": "MIT",
|
||||
"esm": {
|
||||
"cjs": true
|
||||
},
|
||||
"dependencies": {
|
||||
"ejs": "^2.6.1",
|
||||
"esm": "^3.0.82",
|
||||
"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",
|
||||
"poi": "^10.2.10"
|
||||
}
|
||||
}
|
||||
8
packages/search-site/poi.config.js
Normal file
8
packages/search-site/poi.config.js
Normal 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')()],
|
||||
};
|
||||
138
packages/search-site/src/components/SearchForm.vue
Normal file
138
packages/search-site/src/components/SearchForm.vue
Normal 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>
|
||||
75
packages/search-site/src/components/StrainCard.vue
Normal file
75
packages/search-site/src/components/StrainCard.vue
Normal 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>
|
||||
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 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>
|
||||
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>
|
||||
33
packages/search-site/src/index.mjs
Normal file
33
packages/search-site/src/index.mjs
Normal file
@@ -0,0 +1,33 @@
|
||||
import Vue from 'vue';
|
||||
import Router from 'vue-router';
|
||||
|
||||
Vue.use(Router);
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
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')]),
|
||||
});
|
||||
|
||||
export default app;
|
||||
5
packages/search-site/src/lib/emitter.mjs
Normal file
5
packages/search-site/src/lib/emitter.mjs
Normal file
@@ -0,0 +1,5 @@
|
||||
import mitt from 'mitt';
|
||||
|
||||
const emitter = mitt();
|
||||
|
||||
export default emitter;
|
||||
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)),
|
||||
};
|
||||
149
packages/search-site/src/pages/Home.vue
Normal file
149
packages/search-site/src/pages/Home.vue
Normal 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>
|
||||
Reference in New Issue
Block a user