1 Commits

22 changed files with 552 additions and 4379 deletions

View File

@@ -1,3 +0,0 @@
### Authors
- joe fleming ([w33ble](https://github.com/w33ble))

View File

@@ -1,24 +0,0 @@
### Changelog
#### [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)

View File

@@ -1,16 +1,14 @@
{
"name": "strain-tools",
"version": "1.0.2",
"version": "0.0.0",
"description": "strain tools",
"main": "index",
"module": "index.mjs",
"private": true,
"scripts": {
"lint": "eslint \"packages/*/*.{js,mjs,vue}\" \"packages/*/src/**/*.{js,mjs,vue}\"",
"lint": "eslint \"*.{js,mjs}\" \"src/**/*.{js,mjs}\"",
"precommit": "lint-staged",
"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",
"version": "auto-changelog -p && auto-authors && git add CHANGELOG.md AUTHORS.md",
"start": "node .",
"dev": "nodemon --ignore db.json ."
},

View File

@@ -8,3 +8,5 @@ Clone repo and run the command. Resulting data can be found in `db.json`.
yarn install
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.

View File

@@ -1,6 +1,6 @@
{
"name": "scraper",
"version": "1.0.2",
"name": "leafly-scraper",
"version": "0.0.0",
"private": true,
"description": "scrapes strain info, stores for later reference",
"main": "index",
@@ -29,7 +29,7 @@
},
"dependencies": {
"axios": "^0.18.0",
"esm": "^3.0.82",
"esm": "^3.0.81",
"lodash": "^4.17.10",
"lowdb": "^1.0.0"
},

View File

@@ -8,14 +8,14 @@ const adapter = new FileAsync('db.json');
const xhr = axios.create({
headers: {
Accept: 'application/json, text/plain, */*',
Referer: 'https://www.leafly.com/explore/sort-alpha',
Referer: 'https://www.leafly.com/explore',
},
});
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}/sort-alpha`;
const url = `https://www.leafly.com/explore/page-${num}`;
const response = await xhr.get(url, {
responseType: 'json',
});

View File

@@ -1,23 +0,0 @@
{
"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

@@ -6,12 +6,6 @@ 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.
## 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)

0
packages/search-site/dist/.empty vendored Normal file
View File

View File

@@ -1,14 +1,15 @@
{
"name": "search-site",
"version": "1.0.2",
"version": "0.0.0",
"private": true,
"main": "index",
"module": "index.mjs",
"description": "strain search static website",
"scripts": {
"start": "poi",
"build": "poi build",
"deploy": "npm run build && firebase deploy"
"start": "node . serve",
"build": "node . build",
"dev": "nodemon -i dist/ -w src -e mjs,ejs -x 'node . build'",
"deploy": "node . build && firebase deploy"
},
"author": "joe fleming (https://github.com/w33ble)",
"license": "MIT",
@@ -17,19 +18,10 @@
},
"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"
"esm": "^3.0.81"
},
"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"
"nodemon": "^1.18.4"
}
}

View File

@@ -1,8 +0,0 @@
/* 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

@@ -1,138 +0,0 @@
<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

@@ -1,75 +0,0 @@
<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

@@ -1,62 +0,0 @@
<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

@@ -1,22 +0,0 @@
<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

@@ -0,0 +1,364 @@
<!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;
}
@media screen and (min-width: 769px) {
#strain-list .cards .card {
width: 45%;
}
}
@media screen and (min-width: 1280px) {
#strain-list .cards .card {
width: 30%;
}
}
#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 }, 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);
// 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', {
props: {
strain: Object,
},
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>
</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', msg => this.error = msg);
},
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();
}
},
});
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) {
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,33 +1,106 @@
import Vue from 'vue';
import Router from 'vue-router';
import http from 'http';
import fs from 'fs';
import ejs from 'ejs';
Vue.use(Router);
const srcFile = 'src/index.ejs';
const destFile = 'dist/index.html';
Vue.filter('capitalize', value => {
if (!value) return '';
const v = value.toString();
return v.charAt(0).toUpperCase() + v.slice(1);
});
function getData() {
return new Promise((resolve, reject) => {
fs.readFile('../scraper/db.json', (err, str) => {
if (err) reject(err);
else resolve(JSON.parse(str));
});
});
}
Vue.filter('round', (value, digits = 2) => {
const v = parseFloat(value, 10);
return Math.round(v * (10 * digits)) / (10 * digits);
});
async function build() {
const data = await getData();
const options = {};
const router = new Router({
mode: 'history',
routes: [
{
path: '/',
name: 'home',
component: () => import(/* webpackChunkName: "homeapp" */ './pages/Home.vue'),
},
],
});
// calculate adjusted ratings
const totalMean =
data.strains.reduce((acc, strain) => {
return acc + strain.rating + strain.rating_count;
}, 0) / data.strains.length;
const app = new Vue({
router,
render: h => h('div', { attrs: { id: 'app' } }, [h('router-view')]),
});
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,
});
});
export default app;
// 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() {
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);
}
}

View File

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

View File

@@ -1,18 +0,0 @@
/* 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)),
};

View File

@@ -1,149 +0,0 @@
<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>

View File

@@ -1,26 +0,0 @@
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();

3839
yarn.lock

File diff suppressed because it is too large Load Diff