Basic PoC

This commit is contained in:
Lucas Verney 2017-10-22 21:20:58 +02:00
parent ff591aafb1
commit b600d97e44
11 changed files with 10559 additions and 169 deletions

View File

@ -45,5 +45,9 @@ module.exports = {
"ignorePropertyModificationsFor": ["state"]
}
],
// enforce consistent line breaks inside function parentheses
'function-paren-newline': 0,
// Use dangling _
'no-underscore-dangle': 0,
}
}
};

10393
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -16,41 +16,45 @@
"dependencies": {
"es6-promise": "^4.1.1",
"isomorphic-fetch": "^2.2.1",
"vue": "^2.4.2",
"vue-router": "^2.7.0",
"vuetify": "^0.15.7",
"vuex": "^2.4.1"
"vue": "^2.5.2",
"vue-router": "^3.0.1",
"vuetify": "^0.16.9",
"vuex": "^3.0.0"
},
"devDependencies": {
"autoprefixer": "^7.1.2",
"autoprefixer": "^7.1.6",
"babel-core": "^6.22.1",
"babel-eslint": "^7.1.1",
"babel-eslint": "^8.0.1",
"babel-loader": "^7.1.1",
"babel-plugin-istanbul": "^4.1.1",
"babel-plugin-transform-runtime": "^6.22.0",
"babel-preset-env": "^1.3.2",
"babel-preset-env": "^1.6.1",
"babel-preset-stage-2": "^6.22.0",
"babel-register": "^6.22.0",
"chalk": "^2.0.1",
"connect-history-api-fallback": "^1.3.0",
"copy-webpack-plugin": "^4.0.1",
"chai": "^4.1.2",
"chalk": "^2.2.0",
"chromedriver": "^2.33.2",
"connect-history-api-fallback": "^1.4.0",
"copy-webpack-plugin": "^4.2.0",
"cross-env": "^5.1.0",
"cross-spawn": "^5.0.1",
"css-loader": "^0.28.0",
"cssnano": "^3.10.0",
"eslint": "^3.19.0",
"eslint": "^4.9.0",
"eslint-config-airbnb-base": "^12.1.0",
"eslint-friendly-formatter": "^3.0.0",
"eslint-import-resolver-webpack": "^0.8.1",
"eslint-loader": "^1.7.1",
"eslint-plugin-html": "^3.0.0",
"eslint-config-airbnb-base": "^11.1.3",
"eslint-import-resolver-webpack": "^0.8.1",
"eslint-plugin-import": "^2.2.0",
"eslint-plugin-import": "^2.8.0",
"eventsource-polyfill": "^0.9.6",
"express": "^4.14.1",
"extract-text-webpack-plugin": "^2.0.0",
"file-loader": "^0.11.1",
"express": "^4.16.2",
"extract-text-webpack-plugin": "^3.0.1",
"file-loader": "^1.1.5",
"friendly-errors-webpack-plugin": "^1.1.3",
"html-webpack-plugin": "^2.28.0",
"http-proxy-middleware": "^0.17.3",
"webpack-bundle-analyzer": "^2.2.1",
"cross-env": "^5.0.1",
"inject-loader": "^3.0.0",
"karma": "^1.4.1",
"karma-coverage": "^1.1.1",
"karma-mocha": "^1.3.0",
@ -59,31 +63,27 @@
"karma-sinon-chai": "^1.3.1",
"karma-sourcemap-loader": "^0.3.7",
"karma-spec-reporter": "0.0.31",
"karma-webpack": "^2.0.2",
"mocha": "^3.2.0",
"chai": "^3.5.0",
"sinon": "^2.1.0",
"sinon-chai": "^2.8.0",
"inject-loader": "^3.0.0",
"babel-plugin-istanbul": "^4.1.1",
"phantomjs-prebuilt": "^2.1.14",
"chromedriver": "^2.27.2",
"cross-spawn": "^5.0.1",
"karma-webpack": "^2.0.5",
"mocha": "^4.0.1",
"nightwatch": "^0.9.12",
"selenium-server": "^3.0.1",
"opn": "^5.1.0",
"optimize-css-assets-webpack-plugin": "^3.2.0",
"ora": "^1.2.0",
"phantomjs-prebuilt": "^2.1.14",
"rimraf": "^2.6.0",
"selenium-server": "^3.6.0",
"semver": "^5.3.0",
"shelljs": "^0.7.6",
"opn": "^5.1.0",
"optimize-css-assets-webpack-plugin": "^2.0.0",
"ora": "^1.2.0",
"rimraf": "^2.6.0",
"url-loader": "^0.5.8",
"vue-loader": "^13.0.4",
"sinon": "^4.0.1",
"sinon-chai": "^2.14.0",
"url-loader": "^0.6.2",
"vue-loader": "^13.3.0",
"vue-style-loader": "^3.0.1",
"vue-template-compiler": "^2.4.2",
"webpack": "^2.6.1",
"vue-template-compiler": "^2.5.2",
"webpack": "^3.8.1",
"webpack-bundle-analyzer": "^2.2.1",
"webpack-dev-middleware": "^1.10.0",
"webpack-hot-middleware": "^2.18.0",
"webpack-hot-middleware": "^2.20.0",
"webpack-merge": "^4.1.0"
},
"engines": {

View File

@ -1,23 +1,18 @@
<template>
<v-app toolbar>
<NavigationDrawer v-model="isDrawerVisible"/>
<v-toolbar class="indigo" dark fixed>
<v-toolbar color="indigo" dark fixed app clipped-left>
<v-toolbar-side-icon @click.stop="showHideDrawer"></v-toolbar-side-icon>
<v-toolbar-title>HungerGames</v-toolbar-title>
<v-spacer></v-spacer>
<v-menu bottom left offset-y>
<v-btn icon slot="activator">
<v-icon>more_vert</v-icon>
</v-btn>
<v-list>
<v-list-tile>
<v-list-tile-title>Skip quest</v-list-tile-title>
</v-list-tile>
</v-list>
</v-menu>
<v-btn icon>
<v-icon>skip_next</v-icon>
</v-btn>
</v-toolbar>
<main>
<router-view></router-view>
<v-content>
<router-view></router-view>
</v-content>
</main>
</v-app>
</template>

View File

@ -3,44 +3,84 @@ require('isomorphic-fetch');
export const BASEURL = 'https://world.openfoodfacts.org/';
function _fetchFromOFFApi(filters) {
let url = BASEURL;
filters.forEach((filter) => {
url += `/${filter}`;
});
return fetch(`${url}.json`);
}
function missingCategories() {
return fetch(
`${BASEURL}state/categories-to-be-completed.json`,
)
return _fetchFromOFFApi([
'state/product-name-completed',
'state/brands-completed',
'state/photos-validated',
'state/categories-to-be-completed',
])
// Parse JSON
.then(response => response.json())
// Keep only useful data
.then(response => response.products.map(product => ({
id: product.id,
name: product.product_name,
icon: product.image_front_url,
brands: product.brands,
predictedCategories: {
'en:fresh-foods': {
name: 'Fresh foods',
isOK: true,
},
'en:meats': {
name: 'Meats',
isOK: true,
},
'en:prepared-meats': {
name: 'Prepared meats',
isOK: true,
},
'en:hams': {
name: 'Hams',
isOK: true,
},
'fr:charcuteries-crues': {
name: 'Charcuteries crues',
isOK: true,
},
'en:beverages': {
name: 'Beverages',
isOK: true,
},
},
})))
// Predict categories
.then(response => fetch(
'http://localhost:4242/predict',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(response),
},
))
// Parse JSON
.then(response => response.json())
// Parse data, strip products without any predicted category
.then(response => response.data.filter(
product => product.predictedCategories.length > 0),
)
// Augment predicted categories data
.then(response => response.map(
(product) => {
const augmentedProduct = product;
augmentedProduct.predictedCategories = product.predictedCategories.map(
category => ({ name: category, isOk: false }),
);
return augmentedProduct;
},
))
.catch(exc => console.error(`Unable to fetch products with missing categories: ${exc}.`));
}
export { missingCategories };
function missingBrands() {
return fetch(`${BASEURL}state/brands-to-be-completed.json`)
.then(response => response.json())
.then(response => response.products.map(product => ({
id: product.id,
name: product.product_name,
icon: product.image_front_url,
})))
.catch(exc => console.error(`Unable to fetch products with missing brands: ${exc}.`));
}
function missingProductName() {
return fetch(`${BASEURL}state/product-name-to-be-completed.json`)
.then(response => response.json())
.then(response => response.products.map(product => ({
id: product.id,
brands: product.brands,
icon: product.image_front_url,
})))
.catch(exc => console.error(`Unable to fetch products with missing product name: ${exc}.`));
}
export { missingBrands, missingCategories, missingProductName };

View File

@ -2,6 +2,7 @@
<v-navigation-drawer
persistent
clipped
app
enable-resize-watcher
v-model="isActive"
>
@ -14,6 +15,14 @@
<v-list-tile-title>Play!</v-list-tile-title>
</v-list-tile-content>
</v-list-tile>
<v-list-tile :to="{ name: 'About' }" exact>
<v-list-tile-action>
<v-icon>info</v-icon>
</v-list-tile-action>
<v-list-tile-content>
<v-list-tile-title>About</v-list-tile-title>
</v-list-tile-content>
</v-list-tile>
</v-list>
</v-navigation-drawer>
</template>

View File

@ -13,6 +13,7 @@
</v-avatar>
<v-card>
<v-card-text>
<!-- TODO: Preload images -->
<img style="width: 100%;" :src="data.icon" />
</v-card-text>
</v-card>
@ -20,21 +21,21 @@
</v-flex>
<v-flex xs7 class="text-xs-center">
<div>
<div class="headline">{{ data.brands }}</div>
<h2 class="title">{{ data.name }}</h2>
<div class="headline">{{ data.brands || 'Unknown' }}</div>
<h2 class="title">{{ data.name || 'Unkown' }}</h2>
</div>
</v-flex>
</v-layout>
<v-divider />
<v-layout row>
<v-flex xs12>
<h2 class="title">Unselect all incorrect categories:</h2>
<h2 class="title">Select all correct categories:</h2>
</v-flex>
</v-layout>
<v-layout row wrap>
<v-btn
v-for="(category, key) in data.predictedCategories" :key="key"
round primary dark :class="{ green: category.isOK, red: !category.isOK }"
round dark :class="{ green: category.isOK, red: !category.isOK }"
@click.stop="data.predictedCategories[key].isOK = !data.predictedCategories[key].isOK"
>
{{ category.name }}
@ -69,4 +70,8 @@ export default {
.pointable:hover {
opacity: 0.8;
}
.icon {
border-radius: 0;
}
</style>

View File

@ -1,5 +1,6 @@
import Vue from 'vue';
import Router from 'vue-router';
import About from '@/views/About';
import Quest from '@/views/Quest';
Vue.use(Router);
@ -8,8 +9,13 @@ export default new Router({
routes: [
{
path: '/',
name: 'Quest',
name: 'Home',
component: Quest,
},
{
path: '/about',
name: 'About',
component: About,
},
],
});

19
src/views/About.vue Normal file
View File

@ -0,0 +1,19 @@
<template>
<v-container fluid>
<v-layout row>
<v-flex xs12>
<h2>About</h2>
</v-flex>
</v-layout>
<v-layout row>
<v-flex xs12>
<p>TODO</p>
</v-flex>
</v-layout>
</v-container>
</template>
<script>
export default {
};
</script>

View File

@ -6,6 +6,7 @@
import QuestMissingCategories from '@/components/QuestMissingCategories/index';
// TODO: Changing route should not flash old product
export default {
components: {
QuestMissingCategories,
@ -13,10 +14,6 @@ export default {
created() {
this.fetchData();
},
watch: {
// Fetch again when the component is updated
$route: 'fetchData',
},
methods: {
fetchData() {
this.$store.dispatch('preloadQuests');

View File

@ -1,78 +0,0 @@
<template>
<v-container fluid grid-list-lg class="text-xs-center">
<v-layout row mb-3 align-center>
<v-flex xs5>
<v-avatar size="100%">
<img class="icon" src="https://static.openfoodfacts.org/images/products/761/303/423/2465/front_fr.6.400.jpg" />
</v-avatar>
</v-flex>
<v-flex xs7 class="text-xs-center">
<div>
<div class="headline">Herta</div>
<h2 class="title">Le Bon Paris, À l'Étouffée (6 Tranches)</h2>
</div>
</v-flex>
</v-layout>
<v-divider />
<v-layout row>
<v-flex xs12>
<h2 class="title">Fix ingredients list:</h2>
</v-flex>
</v-layout>
<v-layout row wrap>
<v-flex xs12>
<img style="max-width: 100%;" src="https://static.openfoodfacts.org/images/products/761/303/423/2465/ingredients_fr.8.400.jpg" />
</v-flex>
</v-layout>
<v-layout row>
<v-flex xs12>
<v-text-field
value="Jambon frais de porc, Bouillon (eau, couennes de porc, oignons, os de porc, carottes, sel, persil, ail, clou de girofle, poivre, laurier) ; sel, dextrose de maïs, arômes naturels, Conservateur (nitrite de sodium) ; Antioxydant (isoascorbate de sodium)."
class="input-group--focused"
multi-line
/>
</v-text-field>
</v-flex>
</v-layout>
<v-layout row>
<v-flex xs12>
<v-btn>submit</v-btn>
</v-flex>
</v-layout>
</v-container>
</template>
<script>
export default {
data() {
return {
items: {
'en:fresh-foods': {
name: 'Fresh foods',
isOK: true,
},
'en:meats': {
name: 'Meats',
isOK: true,
},
'en:prepared-meats': {
name: 'Prepared meats',
isOK: true,
},
'en:hams': {
name: 'Hams',
isOK: true,
},
'fr:charcuteries-crues': {
name: 'Charcuteries crûes',
isOK: false,
},
'en:beverages': {
name: 'Beverages',
isOK: false,
},
},
};
},
};
</script>