Этот коммит содержится в:
Lucas Verney 2017-11-21 22:11:58 +01:00
родитель a5ed530081
Коммит b57bdb6b7b
26 изменённых файлов: 679 добавлений и 2655 удалений

Просмотреть файл

@ -3,7 +3,6 @@
<head>
<meta charset="utf-8">
<title>HungerGames</title>
<link href='https://fonts.googleapis.com/css?family=Roboto:300,400,500,700|Material+Icons' rel="stylesheet">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui">
</head>
<body>

2622
package-lock.json сгенерированный

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -16,75 +16,60 @@
"dependencies": {
"es6-promise": "^4.1.1",
"isomorphic-fetch": "^2.2.1",
"vue": "^2.5.2",
"material-design-icons": "^3.0.1",
"vue": "^2.5.8",
"vue-router": "^3.0.1",
"vuetify": "^0.16.9",
"vuex": "^3.0.0"
"vuetify": "^0.17.2",
"vuex": "^3.0.1"
},
"devDependencies": {
"autoprefixer": "^7.1.6",
"babel-core": "^6.22.1",
"babel-eslint": "^8.0.1",
"babel-eslint": "^8.0.2",
"babel-loader": "^7.1.1",
"babel-plugin-istanbul": "^4.1.1",
"babel-plugin-transform-runtime": "^6.22.0",
"babel-preset-env": "^1.6.1",
"babel-preset-stage-2": "^6.22.0",
"babel-register": "^6.22.0",
"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",
"connect-history-api-fallback": "^1.5.0",
"copy-webpack-plugin": "^4.2.1",
"cross-env": "^5.1.1",
"cross-spawn": "^5.0.1",
"css-loader": "^0.28.0",
"cssnano": "^3.10.0",
"eslint": "^4.9.0",
"eslint": "^4.11.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-plugin-html": "^4.0.0",
"eslint-plugin-import": "^2.8.0",
"eventsource-polyfill": "^0.9.6",
"express": "^4.16.2",
"extract-text-webpack-plugin": "^3.0.1",
"extract-text-webpack-plugin": "^3.0.2",
"file-loader": "^1.1.5",
"friendly-errors-webpack-plugin": "^1.1.3",
"html-webpack-plugin": "^2.28.0",
"http-proxy-middleware": "^0.17.3",
"inject-loader": "^3.0.0",
"karma": "^1.4.1",
"karma-coverage": "^1.1.1",
"karma-mocha": "^1.3.0",
"karma-phantomjs-launcher": "^1.0.2",
"karma-phantomjs-shim": "^1.4.0",
"karma-sinon-chai": "^1.3.1",
"karma-sourcemap-loader": "^0.3.7",
"karma-spec-reporter": "0.0.31",
"karma-webpack": "^2.0.5",
"mocha": "^4.0.1",
"nightwatch": "^0.9.12",
"opn": "^5.1.0",
"optimize-css-assets-webpack-plugin": "^3.2.0",
"ora": "^1.2.0",
"phantomjs-prebuilt": "^2.1.14",
"phantomjs-prebuilt": "^2.1.16",
"rimraf": "^2.6.0",
"selenium-server": "^3.6.0",
"selenium-server": "^3.7.1",
"semver": "^5.3.0",
"shelljs": "^0.7.6",
"sinon": "^4.0.1",
"sinon-chai": "^2.14.0",
"shelljs": "^0.7.8",
"url-loader": "^0.6.2",
"vue-loader": "^13.3.0",
"vue-loader": "^13.5.0",
"vue-style-loader": "^3.0.1",
"vue-template-compiler": "^2.5.2",
"vue-template-compiler": "^2.5.8",
"webpack": "^3.8.1",
"webpack-bundle-analyzer": "^2.2.1",
"webpack-bundle-analyzer": "^2.9.1",
"webpack-dev-middleware": "^1.10.0",
"webpack-hot-middleware": "^2.20.0",
"webpack-merge": "^4.1.0"
"webpack-merge": "^4.1.1"
},
"engines": {
"node": ">= 4.0.0",

Просмотреть файл

@ -1,19 +1,13 @@
<template>
<v-app toolbar>
<v-app>
<NavigationDrawer v-model="isDrawerVisible"/>
<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-btn icon>
<v-icon>skip_next</v-icon>
</v-btn>
</v-toolbar>
<main>
<v-content>
<router-view></router-view>
</v-content>
</main>
<v-content class="max-width">
<router-view></router-view>
</v-content>
</v-app>
</template>
@ -36,3 +30,11 @@ export default {
},
};
</script>
<style scoped>
.max-width {
max-width: 1000px;
margin-left: auto;
margin-right: auto;
}
</style>

Просмотреть файл

@ -1,15 +1,33 @@
require('es6-promise').polyfill();
require('isomorphic-fetch');
export const BASEURL = 'https://world.openfoodfacts.org/';
export const BASE_URL = 'https://world.openfoodfacts.org';
export const BASE_FETCH_PARAMS = {
/*
credentials: 'include',
headers: {
Authorization: `Basic ${btoa('off:off')}`,
},
*/
};
export const USER_ID = '';
export const PASSWORD = '';
function _fetchFromOFFApi(filters) {
let url = BASEURL;
let url = BASE_URL;
filters.forEach((filter) => {
url += `/${filter}`;
});
return fetch(`${url}.json`);
return fetch(`${url}.json`, BASE_FETCH_PARAMS);
}
function _sendToOFFApi(barcode, fields) {
let url = `${BASE_URL}/cgi/product_jqmp2.pl?code=${barcode}&user_id=${USER_ID}&password=${PASSWORD}`;
Object.keys(fields).forEach((field) => {
url += `&${field}=${fields[field]}`;
});
return fetch(url, BASE_FETCH_PARAMS);
}
function missingCategories() {
@ -60,8 +78,9 @@ function missingCategories() {
function updateCategories(productId, categories) {
// TODO
console.log(productId, categories);
return _sendToOFFApi(productId, {
categories: categories.join(','),
});
}

24
src/components/About.vue Обычный файл
Просмотреть файл

@ -0,0 +1,24 @@
<template>
<v-container fluid>
<v-layout row>
<v-flex xs12 class="text-xs-center">
<h2>About</h2>
</v-flex>
</v-layout>
<v-layout row>
<v-flex xs12>
<p>
This app is a quest-based app to help you update and
complete <a href="http://openfoodfacts.org/">OpenFoodFacts</a> data.
</p>
<p>This app is a free software, released under MIT license.</p>
<p>OpenFoodFacts database is a free database licensed under <a href="https://world.openfoodfacts.org/legal">Open Database License</a>.</p>
</v-flex>
</v-layout>
</v-container>
</template>
<script>
export default {
};
</script>

Просмотреть файл

@ -1,9 +1,9 @@
<template>
<v-navigation-drawer
persistent
fixed
clipped
app
enable-resize-watcher
disable-resize-watcher
v-model="isActive"
>
<v-list>

Просмотреть файл

@ -1,89 +0,0 @@
<template>
<v-container fluid grid-list-lg class="text-xs-center">
<v-layout row mb-3 align-center>
<v-flex xs5>
<v-dialog v-model="dialog" lazy absolute>
<v-avatar
size="100%"
class="pointable"
slot="activator"
>
<!-- TODO: Should be closable by ESC -->
<img class="icon" :src="data.icon" />
</v-avatar>
<v-card>
<v-card-text>
<!-- TODO: Preload images -->
<img style="width: 100%;" :src="data.icon" />
</v-card-text>
</v-card>
</v-dialog>
</v-flex>
<v-flex xs7 class="text-xs-center">
<div>
<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">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 dark :class="{ green: category.isOK, red: !category.isOK }"
@click.stop="data.predictedCategories[key].isOK = !data.predictedCategories[key].isOK"
>
{{ category.name }}
</v-btn>
</v-layout>
<v-layout row>
<v-flex xs12>
<v-btn @click="handleSubmit()">Submit</v-btn>
</v-flex>
</v-layout>
</v-container>
</template>
<script>
export default {
props: {
data: Object,
onSubmit: Function,
},
data() {
return {
dialog: false,
};
},
methods: {
handleSubmit() {
const okCategories = [];
Object.keys(this.data.predictedCategories).forEach((key) => {
if (this.data.predictedCategories[key].isOk) {
okCategories.push(key);
}
});
this.onSubmit(okCategories);
},
},
};
</script>
<style scoped>
.pointable {
cursor: pointer;
}
.pointable:hover {
opacity: 0.8;
}
.icon {
border-radius: 0;
}
</style>

Просмотреть файл

@ -0,0 +1,66 @@
<template>
<v-container fluid grid-list-lg class="text-xs-center">
<v-layout row mb-3 align-center>
<v-flex xs5>
<ZoomableImage :url="questData.icon" />
</v-flex>
<v-flex xs7 class="text-xs-center">
<div>
<div class="headline">{{ questData.brands || 'Unknown' }}</div>
<h2 class="title">{{ questData.name || 'Unkown' }}</h2>
</div>
</v-flex>
</v-layout>
<v-divider />
<v-layout row>
<v-flex xs12>
<h2 class="title">Select all correct categories:</h2>
</v-flex>
</v-layout>
<v-layout row wrap>
<v-btn
v-for="(category, key) in questData.predictedCategories" :key="key"
round dark :class="{ green: category.isOk, red: !category.isOk }"
@click.stop="handleClick(key)"
>
{{ category.name }}
</v-btn>
</v-layout>
<v-layout row>
<v-flex xs12>
<v-btn @click="handleSubmit()">Submit</v-btn>
or <a @click="onSkip()">Skip</a>
</v-flex>
</v-layout>
</v-container>
</template>
<script>
import ZoomableImage from '@/components/ZoomableImage';
export default {
components: {
ZoomableImage,
},
props: {
questData: Object,
onSubmit: Function,
onSkip: Function,
},
methods: {
handleSubmit() {
const okCategories = [];
Object.keys(this.questData.predictedCategories).forEach((key) => {
if (this.questData.predictedCategories[key].isOk) {
okCategories.push(this.questData.predictedCategories[key].name);
}
});
this.onSubmit(okCategories);
},
handleClick(key) {
this.questData.predictedCategories[key].isOk =
!this.questData.predictedCategories[key].isOk;
},
},
};
</script>

57
src/components/Quests/index.vue Обычный файл
Просмотреть файл

@ -0,0 +1,57 @@
<template>
<div>
<v-layout row v-if="isLoading">
<v-flex xs12>
<p>Loading quests</p>
</v-flex>
</v-layout>
<template v-else>
<QuestMissingCategories v-if="questData" :questData="questData" :onSubmit="validateQuest" :onSkip="skipQuest" />
</template>
</div>
</template>
<script>
import QuestMissingCategories from '@/components/Quests/QuestMissingCategories/index';
export default {
data() {
return {
isLoading: false,
error: null,
};
},
components: {
QuestMissingCategories,
},
created() {
this.fetchData();
},
methods: {
fetchData() {
this.isLoading = true;
this.$store.dispatch('preloadQuests').then(() => {
this.isLoading = false;
});
},
validateQuest(solution) {
this.$store.dispatch('validateQuest', {
type: this.questData.type,
id: this.questData.id,
solution,
});
},
skipQuest() {
this.$store.dispatch('skipQuest', {
type: this.questData.type,
id: this.questData.id,
});
},
},
computed: {
questData() {
return this.$store.getters.popQuest;
},
},
};
</script>

72
src/components/ZoomableImage.vue Обычный файл
Просмотреть файл

@ -0,0 +1,72 @@
<template>
<v-dialog v-model="dialog">
<v-avatar
size="100%"
class="pointable"
slot="activator"
>
<!-- TODO: Should be closable by ESC -->
<img class="icon" :src="url" />
</v-avatar>
<v-card>
<v-card-text>
<!-- TODO: Preload images -->
<img class="zoomed-image" :src="url" />
</v-card-text>
</v-card>
</v-dialog>
</template>
<script>
export default {
props: {
url: String,
},
mounted() {
document.addEventListener('keydown', this.handleEscKey);
},
beforeDestroy() {
document.removeEventListener('keydown', this.handleEscKey);
},
methods: {
handleEscKey(e) {
if (
this.dialog && (
e.key === 'Escape' ||
e.key === 'Esc' ||
e.keyCode === 27
)
) {
this.dialog = false;
}
},
},
data() {
return {
dialog: false,
};
},
};
</script>
<style scoped>
.pointable {
cursor: pointer;
}
.pointable:hover {
opacity: 0.8;
}
.icon {
border-radius: 0;
}
.zoomed-image {
margin-left: auto;
margin-right: auto;
max-width: 100%;
max-height: 100%;
display: block;
}
</style>

Просмотреть файл

@ -3,6 +3,7 @@
import Vue from 'vue';
import Vuetify from 'vuetify';
import 'vuetify/dist/vuetify.min.css';
import 'material-design-icons/iconfont/material-icons.css';
import App from './App';
import router from './router';

Просмотреть файл

@ -1,7 +1,8 @@
import Vue from 'vue';
import Router from 'vue-router';
import About from '@/views/About';
import Quest from '@/views/Quest';
import About from '@/components/About';
import Quests from '@/components/Quests';
Vue.use(Router);
@ -10,7 +11,7 @@ export default new Router({
{
path: '/',
name: 'Home',
component: Quest,
component: Quests,
},
{
path: '/about',

Просмотреть файл

@ -4,25 +4,28 @@ import quests from './quests';
export default {
preloadQuests({ commit }) {
commit(types.IS_LOADING_QUESTS);
const apiRequests = [];
Object.keys(quests).forEach((quest) => {
quests[quest]().then(
items => commit(
types.STORE_QUESTS_ITEMS,
{
type: 'missingCategories',
items,
},
apiRequests.push(
quests[quest]().then(
items => commit(
types.STORE_QUESTS_ITEMS,
{
type: 'missingCategories',
items,
},
),
),
);
});
return Promise.all(apiRequests);
},
validateQuest({ commit }, { type, id, solution }) {
if (type === 'missingCategories') {
updateCategories(id, solution).then(() => commit(
types.VALIDATE_QUEST,
types.REMOVE_QUEST_ITEM,
{
type,
id,
@ -30,4 +33,14 @@ export default {
));
}
},
skipQuest({ commit }, { type, id }) {
commit(
types.REMOVE_QUEST_ITEM,
{
type,
id,
},
);
},
};

Просмотреть файл

@ -1,3 +1,2 @@
export const IS_LOADING_QUESTS = 'IS_LOADING_QUESTS';
export const STORE_QUESTS_ITEMS = 'STORE_QUESTS_ITEMS';
export const VALIDATE_QUEST = 'VALIDATE_QUEST';
export const REMOVE_QUEST_ITEM = 'REMOVE_QUEST_ITEM';

Просмотреть файл

@ -3,20 +3,19 @@ import Vue from 'vue';
import * as types from './mutations-types';
export const initialState = {
isLoading: false,
questsItems: {},
};
export const mutations = {
[types.IS_LOADING_QUESTS](state) {
state.isLoading = true;
},
[types.STORE_QUESTS_ITEMS](state, { type, items }) {
Vue.set(state.questsItems, type, items);
},
[types.VALIDATE_QUEST](state, { id, type }) {
[types.REMOVE_QUEST_ITEM](state, { type, id }) {
const items = state.questsItems[type];
delete items[id];
const index = items.findIndex(item => item.id === id);
if (index > -1) {
items.splice(index, 1);
}
Vue.set(state.questsItems, type, items);
},
};

Просмотреть файл

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

Просмотреть файл

@ -1,35 +0,0 @@
<template>
<QuestMissingCategories v-if="questData" :data="questData" :onSubmit="validateQuest" />
</template>
<script>
import QuestMissingCategories from '@/components/QuestMissingCategories/index';
// TODO: Changing route should not flash old product
export default {
components: {
QuestMissingCategories,
},
created() {
this.fetchData();
},
methods: {
fetchData() {
this.$store.dispatch('preloadQuests');
},
validateQuest(solution) {
this.$store.dispatch('validateQuest', {
type: this.questData.type,
id: this.questData.id,
solution,
});
},
},
computed: {
questData() {
return this.$store.getters.popQuest;
},
},
};
</script>

Просмотреть файл

@ -1,24 +0,0 @@
// A custom Nightwatch assertion.
// the name of the method is the filename.
// can be used in tests like this:
//
// browser.assert.elementCount(selector, count)
//
// for how to write custom assertions see
// http://nightwatchjs.org/guide#writing-custom-assertions
exports.assertion = function (selector, count) {
this.message = `Testing if element <${selector}> has count: ${count}`;
this.expected = count;
this.pass = function (val) {
return val === this.expected;
};
this.value = function (res) {
return res.value;
};
this.command = function (cb) {
const self = this;
return this.api.execute(selector => document.querySelectorAll(selector).length, [selector], (res) => {
cb.call(self, res);
});
};
};

Просмотреть файл

@ -1,46 +0,0 @@
require('babel-register');
const config = require('../../config');
// http://nightwatchjs.org/gettingstarted#settings-file
module.exports = {
src_folders: ['test/e2e/specs'],
output_folder: 'test/e2e/reports',
custom_assertions_path: ['test/e2e/custom-assertions'],
selenium: {
start_process: true,
server_path: require('selenium-server').path,
host: '127.0.0.1',
port: 4444,
cli_args: {
'webdriver.chrome.driver': require('chromedriver').path,
},
},
test_settings: {
default: {
selenium_port: 4444,
selenium_host: 'localhost',
silent: true,
globals: {
devServerURL: `http://localhost:${process.env.PORT || config.dev.port}`,
},
},
chrome: {
desiredCapabilities: {
browserName: 'chrome',
javascriptEnabled: true,
acceptSslCerts: true,
},
},
firefox: {
desiredCapabilities: {
browserName: 'firefox',
javascriptEnabled: true,
acceptSslCerts: true,
},
},
},
};

Просмотреть файл

@ -1,33 +0,0 @@
// 1. start the dev server using production config
process.env.NODE_ENV = 'testing';
const server = require('../../build/dev-server.js');
server.ready.then(() => {
// 2. run the nightwatch test suite against it
// to run in additional browsers:
// 1. add an entry in test/e2e/nightwatch.conf.json under "test_settings"
// 2. add it to the --env flag below
// or override the environment flag, for example: `npm run e2e -- --env chrome,firefox`
// For more information on Nightwatch's config file, see
// http://nightwatchjs.org/guide#settings-file
let opts = process.argv.slice(2);
if (opts.indexOf('--config') === -1) {
opts = opts.concat(['--config', 'test/e2e/nightwatch.conf.js']);
}
if (opts.indexOf('--env') === -1) {
opts = opts.concat(['--env', 'chrome']);
}
const spawn = require('cross-spawn');
const runner = spawn('./node_modules/.bin/nightwatch', opts, { stdio: 'inherit' });
runner.on('exit', (code) => {
server.close();
process.exit(code);
});
runner.on('error', (err) => {
server.close();
throw err;
});
});

Просмотреть файл

@ -1,19 +0,0 @@
// For authoring Nightwatch tests, see
// http://nightwatchjs.org/guide#usage
module.exports = {
'default e2e tests': function test(browser) {
// automatically uses dev Server port from /config.index.js
// default: http://localhost:8080
// see nightwatch.conf.js
const devServer = browser.globals.devServerURL;
browser
.url(devServer)
.waitForElementVisible('#app', 5000)
.assert.elementPresent('.hello')
.assert.containsText('h1', 'Welcome to Your Vue.js App')
.assert.elementCount('img', 1)
.end();
},
};

Просмотреть файл

@ -1,9 +0,0 @@
{
"env": {
"mocha": true
},
"globals": {
"expect": true,
"sinon": true
}
}

Просмотреть файл

@ -1,13 +0,0 @@
import Vue from 'vue';
Vue.config.productionTip = false;
// require all test files (files that ends with .spec.js)
const testsContext = require.context('./specs', true, /\.spec$/);
testsContext.keys().forEach(testsContext);
// require all src files except main.js for coverage.
// you can also change this to match only the subset of files that
// you want coverage for.
const srcContext = require.context('../../src', true, /^\.\/(?!main(\.js)?$)/);
srcContext.keys().forEach(srcContext);

Просмотреть файл

@ -1,33 +0,0 @@
// This is a karma config file. For more details see
// http://karma-runner.github.io/0.13/config/configuration-file.html
// we are also using it with karma-webpack
// https://github.com/webpack/karma-webpack
const webpackConfig = require('../../build/webpack.test.conf');
module.exports = function (config) {
config.set({
// to run in additional browsers:
// 1. install corresponding karma launcher
// http://karma-runner.github.io/0.13/config/browsers.html
// 2. add it to the `browsers` array below.
browsers: ['PhantomJS'],
frameworks: ['mocha', 'sinon-chai', 'phantomjs-shim'],
reporters: ['spec', 'coverage'],
files: ['./index.js'],
preprocessors: {
'./index.js': ['webpack', 'sourcemap'],
},
webpack: webpackConfig,
webpackMiddleware: {
noInfo: true,
},
coverageReporter: {
dir: './coverage',
reporters: [
{ type: 'lcov', subdir: '.' },
{ type: 'text-summary' },
],
},
});
};

Просмотреть файл

@ -1,11 +0,0 @@
import Vue from 'vue';
import Hello from '@/components/Hello';
describe('Hello.vue', () => {
it('should render correct contents', () => {
const Constructor = Vue.extend(Hello);
const vm = new Constructor().$mount();
expect(vm.$el.querySelector('.hello h1').textContent)
.to.equal('Welcome to Your Vue.js App');
});
});