Compare commits
6 Commits
b9e5762213
...
63d02aa3a7
| Author | SHA1 | Date | |
|---|---|---|---|
| 63d02aa3a7 | |||
| fc158da8a1 | |||
| 804fac1afb | |||
| 22a95c3fe8 | |||
| 699a03c7c0 | |||
| 37dcabde5e |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -7,6 +7,8 @@ yarn-error.log
|
|||||||
.env
|
.env
|
||||||
coverage
|
coverage
|
||||||
.nyc_output
|
.nyc_output
|
||||||
|
.firebase
|
||||||
|
.firebaserc
|
||||||
coverage.lcov
|
coverage.lcov
|
||||||
/lib
|
dist/
|
||||||
db.json
|
db.json
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.18.0",
|
"axios": "^0.18.0",
|
||||||
"esm": "^3.0.17",
|
"esm": "^3.0.81",
|
||||||
"lodash": "^4.17.10",
|
"lodash": "^4.17.10",
|
||||||
"lowdb": "^1.0.0"
|
"lowdb": "^1.0.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ const xhr = axios.create({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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}`;
|
||||||
const response = await xhr.get(url, {
|
const response = await xhr.get(url, {
|
||||||
@@ -45,13 +47,25 @@ export default async function scrapeLeafly(startFrom = 1, endAt = Infinity) {
|
|||||||
let finished = false;
|
let finished = false;
|
||||||
const db = await low(adapter);
|
const db = await low(adapter);
|
||||||
|
|
||||||
await db.defaults({ strains: [] }).write();
|
async function writeTag(type, tag) {
|
||||||
|
const res = await db
|
||||||
|
.get(type)
|
||||||
|
.indexOf(tag)
|
||||||
|
.value();
|
||||||
|
|
||||||
while (!finished) {
|
if (res < 0) {
|
||||||
console.log(`Fetching page ${pageNum}`);
|
await db
|
||||||
const data = await getPage(pageNum);
|
.get(type)
|
||||||
|
.push(tag)
|
||||||
|
.write();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
data.strains.forEach(async strain => {
|
async function writeTags(type, tags) {
|
||||||
|
await pSeries(tags.map(tag => () => writeTag(type, tag)));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writeDoc(strain) {
|
||||||
// check for value
|
// check for value
|
||||||
const doc = db
|
const doc = db
|
||||||
.get('strains')
|
.get('strains')
|
||||||
@@ -65,8 +79,31 @@ export default async function scrapeLeafly(startFrom = 1, endAt = Infinity) {
|
|||||||
.get('strains')
|
.get('strains')
|
||||||
.push(strain)
|
.push(strain)
|
||||||
.write();
|
.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);
|
||||||
|
|
||||||
|
await pSeries(data.strains.map(strain => () => writeDoc(strain)));
|
||||||
|
|
||||||
if (pageNum >= endAt || !data.strains.length || data.page.isLastPage) finished = true;
|
if (pageNum >= endAt || !data.strains.length || data.page.isLastPage) finished = true;
|
||||||
pageNum += 1;
|
pageNum += 1;
|
||||||
|
|||||||
10
packages/search-site/README.md
Normal file
10
packages/search-site/README.md
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# search-site
|
||||||
|
|
||||||
|
strain search static website.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
#### License
|
||||||
|
|
||||||
|
MIT © [w33ble](https://github.com/w33ble)
|
||||||
0
packages/search-site/dist/.empty
vendored
Normal file
0
packages/search-site/dist/.empty
vendored
Normal file
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();
|
||||||
27
packages/search-site/package.json
Normal file
27
packages/search-site/package.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"name": "search-site",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"main": "index",
|
||||||
|
"module": "index.mjs",
|
||||||
|
"description": "strain search static website",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node . serve",
|
||||||
|
"build": "node . build",
|
||||||
|
"dev": "nodemon -i dist/ -w src -e mjs,ejs -x 'node . build'",
|
||||||
|
"deploy": "node . build && firebase deploy"
|
||||||
|
},
|
||||||
|
"author": "joe fleming (https://github.com/w33ble)",
|
||||||
|
"license": "MIT",
|
||||||
|
"esm": {
|
||||||
|
"cjs": true
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"ejs": "^2.6.1",
|
||||||
|
"esm": "^3.0.81"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"firebase-tools": "^4.2.1",
|
||||||
|
"nodemon": "^1.18.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
264
packages/search-site/src/index.ejs
Normal file
264
packages/search-site/src/index.ejs
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
<!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">
|
||||||
|
</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 cards">
|
||||||
|
<div v-for="strain in strains" class="card">
|
||||||
|
<header class="card-header">
|
||||||
|
<p class="card-header-title">{{strain.name | capitalize}}</p>
|
||||||
|
<div class="tags" style="margin: 0 12px;">
|
||||||
|
<span class="tag is-rounded is-light">{{strain.rating | round}} ({{strain.rating_count}})</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div class="card-content">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<script src="https://unpkg.com/lunr@2.3.1/lunr.js"></script>
|
||||||
|
<script src="https://unpkg.com/vue@2.5.17/dist/vue.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 }) {
|
||||||
|
const data = <%- data %>;
|
||||||
|
const emitter = mitt();
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
});
|
||||||
|
|
||||||
|
// form handler
|
||||||
|
new Vue({
|
||||||
|
el: '#search-form',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
effects: data.effects,
|
||||||
|
uses: data.uses,
|
||||||
|
conditions: data.conditions,
|
||||||
|
flavors: data.flavors,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
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),
|
||||||
|
}
|
||||||
|
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() {
|
||||||
|
emitter.on('search', this.setRequirements);
|
||||||
|
this.updateStrains();
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
emitter.off('search', this.setRequirements);
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
setRequirements(reqs) {
|
||||||
|
// this.requirements = reqs;
|
||||||
|
reqs => console.log('search params', reqs);
|
||||||
|
},
|
||||||
|
updateStrains(limit = 20) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
})(this);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
88
packages/search-site/src/index.mjs
Normal file
88
packages/search-site/src/index.mjs
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import http from 'http';
|
||||||
|
import fs from 'fs';
|
||||||
|
import ejs from 'ejs';
|
||||||
|
|
||||||
|
const srcFile = 'src/index.ejs';
|
||||||
|
const destFile = 'dist/index.html';
|
||||||
|
|
||||||
|
function getData() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
fs.readFile('../scraper/db.json', (err, str) => {
|
||||||
|
if (err) reject(err);
|
||||||
|
else resolve(JSON.parse(str));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function build() {
|
||||||
|
const data = await getData();
|
||||||
|
const options = {};
|
||||||
|
data.strains = data.strains.sort((n, strain) => {
|
||||||
|
if (strain.rating === n.rating) return 0;
|
||||||
|
return strain.rating < n.rating ? -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user