Merge branch 'master' into feat/ci

feat/ci
sundowndev 2019-10-24 23:13:26 +02:00
commit 3d3204e322
25 changed files with 448 additions and 113 deletions

13
client/Dockerfile Normal file
View File

@ -0,0 +1,13 @@
# build stage
FROM node:lts-alpine as build-stage
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# production stage
FROM nginx:stable-alpine as production-stage
COPY --from=build-stage /app/public /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

46
client/nginx.conf Normal file
View File

@ -0,0 +1,46 @@
server {
listen 80;
server_name localhost;
#charset koi8-r;
#access_log /var/log/nginx/host.access.log main;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri /index.html;
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
#
# error_page 500 502 503 504 /50x.html;
# location = /50x.html {
# root /usr/share/nginx/html;
# }
# proxy the PHP scripts to Apache listening on 127.0.0.1:80
#
#location ~ \.php$ {
# proxy_pass http://127.0.0.1;
#}
# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
#
#location ~ \.php$ {
# root html;
# fastcgi_pass 127.0.0.1:9000;
# fastcgi_index index.php;
# fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;
# include fastcgi_params;
#}
# deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
#
#location ~ /\.ht {
# deny all;
#}
}

View File

@ -2120,6 +2120,43 @@
"integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==",
"dev": true "dev": true
}, },
"axios": {
"version": "0.19.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.19.0.tgz",
"integrity": "sha512-1uvKqKQta3KBxIz14F2v06AEHZ/dIoeKfbTRkK1E5oqjDnuEerLmYTgJB5AiQZHJcljpg1TuRzdjDR06qNk0DQ==",
"requires": {
"follow-redirects": "1.5.10",
"is-buffer": "^2.0.2"
},
"dependencies": {
"debug": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"requires": {
"ms": "2.0.0"
}
},
"follow-redirects": {
"version": "1.5.10",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz",
"integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==",
"requires": {
"debug": "=3.1.0"
}
},
"is-buffer": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz",
"integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A=="
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
}
}
},
"babel-code-frame": { "babel-code-frame": {
"version": "6.26.0", "version": "6.26.0",
"resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz",

View File

@ -1,7 +1,7 @@
{ {
"name": "api-directory", "name": "client",
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": false,
"scripts": { "scripts": {
"serve": "vue-cli-service serve", "serve": "vue-cli-service serve",
"build": "vue-cli-service build", "build": "vue-cli-service build",
@ -10,6 +10,7 @@
"test:unit": "vue-cli-service test:unit" "test:unit": "vue-cli-service test:unit"
}, },
"dependencies": { "dependencies": {
"axios": "^0.19.0",
"core-js": "^2.6.5", "core-js": "^2.6.5",
"register-service-worker": "^1.6.2", "register-service-worker": "^1.6.2",
"vue": "^2.6.10", "vue": "^2.6.10",
@ -29,6 +30,7 @@
"node-sass": "^4.9.0", "node-sass": "^4.9.0",
"sass-loader": "^7.1.0", "sass-loader": "^7.1.0",
"ts-jest": "^23.0.0", "ts-jest": "^23.0.0",
"tslint": "^5.20.0",
"typescript": "^3.4.3", "typescript": "^3.4.3",
"vue-template-compiler": "^2.6.10" "vue-template-compiler": "^2.6.10"
} }

View File

@ -5,7 +5,7 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0"> <meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico"> <link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title>api-directory</title> <title>HETIC vs EEMI</title>
</head> </head>
<body> <body>
<noscript> <noscript>

View File

@ -1,16 +1,12 @@
<template> <template>
<div id="app"> <div id="app">
<div id="nav"> <router-view />
<router-link to="/">Home</router-link> |
<router-link to="/about">About</router-link>
</div>
<router-view/>
</div> </div>
</template> </template>
<style lang="scss"> <style lang="scss">
#app { #app {
font-family: 'Avenir', Helvetica, Arial, sans-serif; font-family: "Avenir", Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
text-align: center; text-align: center;

View File

@ -1,63 +0,0 @@
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<p>
For a guide and recipes on how to configure / customize this project,<br>
check out the
<a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
</p>
<h3>Installed CLI Plugins</h3>
<ul>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-typescript" target="_blank" rel="noopener">typescript</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-pwa" target="_blank" rel="noopener">pwa</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-unit-jest" target="_blank" rel="noopener">unit-jest</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-e2e-cypress" target="_blank" rel="noopener">e2e-cypress</a></li>
</ul>
<h3>Essential Links</h3>
<ul>
<li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
<li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
<li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
<li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
<li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
</ul>
<h3>Ecosystem</h3>
<ul>
<li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
<li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
<li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
<li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
<li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
</ul>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
name: 'HelloWorld',
props: {
msg: String,
},
});
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="scss">
h3 {
margin: 40px 0 0;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
</style>

View File

@ -0,0 +1,41 @@
<template>
<div class="score_screen">
<h1>Score: {{ score }}/{{ count }}</h1>
<h3>Bon c'est pas tip top tout ça</h3>
<button>Rejouer</button>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
name: 'ScoreScreen',
data() {
return {
messages: [{ min: 2, msg: 'Bon c\'est pas tip top tout ça' }],
};
},
props: {
score: Number,
count: Number,
},
});
</script>
<style scoped lang="scss">
h3 {
margin: 40px 0 0;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
</style>

View File

@ -0,0 +1,9 @@
export enum EAnswer {
HETIC = 1,
EEMI = 2,
}
export default interface IQuestion {
text: string;
answer: EAnswer;
}

View File

@ -0,0 +1,4 @@
export default interface ISchool {
id: string;
name: string;
}

View File

@ -9,17 +9,9 @@ export default new Router({
base: process.env.BASE_URL, base: process.env.BASE_URL,
routes: [ routes: [
{ {
path: '/', path: '*',
name: 'home', name: 'home',
component: Home, component: Home,
}, },
{
path: '/about',
name: 'about',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "about" */ './views/About.vue'),
},
], ],
}); });

View File

@ -1,16 +0,0 @@
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
export default new Vuex.Store({
state: {
},
mutations: {
},
actions: {
},
});

17
client/src/store/index.ts Normal file
View File

@ -0,0 +1,17 @@
import Vue from 'vue';
import Vuex from 'vuex';
// Modules
import questions from './modules/questions';
Vue.use(Vuex);
const debug = process.env.NODE_ENV !== 'production';
export default new Vuex.Store({
modules: {
questions,
},
strict: debug,
plugins: [],
});

View File

@ -0,0 +1,60 @@
import axios from 'axios';
import IQuestion from '@/models/question';
import { shuffleArray } from '@/utils';
import { Module } from 'vuex';
const questions: Module<
{ index: number; score: number; questions: IQuestion[] },
any
> = {
namespaced: false,
state: {
index: 0,
score: 0,
questions: [],
},
getters: {
index(state) {
return state.index;
},
score(state) {
return state.score;
},
questions(state): IQuestion[] {
return state.questions;
},
currentQuestion(state): IQuestion | null {
return state.questions.length ? state.questions[state.index] : null;
},
checkAnswer(state) {
return ({ answer }: { answer: number }): boolean =>
state.questions[state.index].answer === answer;
},
},
mutations: {
async fetchQuestions(state): Promise<IQuestion[]> {
try {
const res = await axios.get('http://localhost:3000/questions');
return (state.questions = shuffleArray(res.data.questions));
} catch (e) {
throw new Error(e);
}
},
increaseIndex(state: { index: number }): number {
return state.index++;
},
increaseScore(state: { score: number }): number {
return state.score++;
},
},
actions: {
increaseIndex(context): void {
context.commit('increaseIndex');
},
increaseScore(context): void {
context.commit('increaseScore');
},
},
};
export default questions;

12
client/src/utils/index.ts Normal file
View File

@ -0,0 +1,12 @@
const shuffleArray = (arr: unknown[]): any[] => {
for (let i = arr.length - 1; i >= 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
const k = arr[i];
arr[i] = arr[j];
arr[j] = k;
}
return arr;
};
export { shuffleArray };

View File

@ -1,5 +0,0 @@
<template>
<div class="about">
<h1>This is an about page</h1>
</div>
</template>

View File

@ -1,18 +1,71 @@
<template> <template>
<div class="home"> <div class="home">
<img alt="Vue logo" src="../assets/logo.png"> <div v-show="!finished">
<HelloWorld msg="Welcome to Your Vue.js + TypeScript App"/> <h1>HETIC vs EEMI</h1>
<h2>{{ getQuestion.text }}</h2>
<p>Plus Éemien ou Héticien ? ({{ index + 1 }}/{{ getQuestionCount }})</p>
<span v-for="(choice) in choices" :key="choice.id">
<button @click="answer(choice.id)">{{ choice.name }}</button>
</span>
</div>
<ScoreScreen v-show="finished" :score="score" :count="getQuestionCount" />
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from "vue";
import HelloWorld from '@/components/HelloWorld.vue'; // @ is an alias to /src import IQuestion from "../models/question";
import { mapState, mapGetters, mapMutations } from "vuex";
import ScoreScreen from "../components/ScoreScreen.vue";
export default Vue.extend({ export default Vue.extend({
name: 'home', name: "home",
components: { data() {
HelloWorld, return {
choices: [{ id: 1, name: "HETIC" }, { id: 2, name: "EEMI" }],
finished: false
};
}, },
computed: {
...mapGetters([
"questions",
"index",
"score",
"currentQuestion",
"checkAnswer"
]),
...mapMutations(["fetchQuestions", "increaseIndex", "increaseScore"]),
getQuestion(): IQuestion | {} {
return this.$store.getters.currentQuestion || {};
},
getQuestionCount(): number {
return this.$store.getters.questions.length;
}
},
methods: {
answer(answer: number): void {
const index: number = this.$store.getters.index;
const questions: IQuestion[] = this.$store.getters.questions;
if (this.$store.getters.checkAnswer({ answer })) {
this.$store.dispatch("increaseScore");
}
if (questions[index + 1]) {
this.$store.dispatch("increaseIndex");
} else {
this.finished = true;
}
}
},
components: {
ScoreScreen
},
async created() {
await this.fetchQuestions;
}
}); });
</script> </script>

View File

@ -0,0 +1,23 @@
import { shuffleArray } from '@/utils';
describe('Utils - /src/utils/index.ts', () => {
describe('#shuffleArr', () => {
it('should not change array', () => {
const arr = [1];
expect(shuffleArray(arr)).toStrictEqual(arr);
});
it('should have correct length', () => {
const arr = 'the quick brown fox jumps over the lazy dog'.split(' ');
expect(shuffleArray(arr).length).toEqual(9);
});
it('should return an array', () => {
const arr = 'the quick brown fox jumps over the lazy dog'.split(' ');
expect(Array.isArray(shuffleArray(arr))).toEqual(true);
});
});
});

54
docker-compose.yml Normal file
View File

@ -0,0 +1,54 @@
version: '3'
services:
client:
container_name: hve_client
restart: on-failure
build:
context: .
dockerfile: ./Dockerfile
volumes:
- ./client/nginx.conf:/etc/nginx/conf.d/default.conf:ro
environment:
- NODE_ENV=production
networks:
- default
- web
command: ['nginx', '-g', 'daemon off;']
# labels:
# - 'traefik.docker.network=web'
# - 'traefik.enable=true'
# - 'traefik.domain=hve.crvx.fr'
# - 'traefik.basic.frontend.rule=Host:hve.crvx.fr'
# - 'traefik.basic.port=80'
# - 'traefik.basic.protocol=http'
# - 'traefik.frontend.headers.SSLRedirect=true'
# - 'traefik.frontend.headers.STSSeconds=315360000'
# - 'traefik.frontend.headers.browserXSSFilter=true'
# - 'traefik.frontend.headers.contentTypeNosniff=true'
# - 'traefik.frontend.headers.forceSTSHeader=true'
# - "traefik.frontend.headers.contentSecurityPolicy=default-src 'self';frame-ancestors 'self';style-src 'self';script-src 'self';img-src 'self';font-src 'self'"
# - 'traefik.frontend.headers.referrerPolicy=no-referrer'
# - 'traefik.frontend.headers.frameDeny=true'
api:
container_name: hve_api
restart: on-failure
image: node:8
build:
context: .
dockerfile: ./server/Dockerfile
env_file:
- .env
environment:
- NODE_ENV=production
ports:
- '3000:3000'
networks:
- default
- postgres
command: ['node', '/api/server/index.js']
networks:
web:
external: true

7
lerna.json Normal file
View File

@ -0,0 +1,7 @@
{
"packages": [
"./client",
"./server"
],
"version": "0.0.0"
}

29
package.json Normal file
View File

@ -0,0 +1,29 @@
{
"name": "HETICvsEEMI",
"version": "1.0.0",
"description": "Plus Eemien ou Héticien ? Le jeu qui ne fait pas rire les élèves.",
"main": "index.js",
"scripts": {
"bootstrap": "lerna bootstrap",
"server:install": "cd server && npm i",
"client:install": "cd client && npm i",
"client:build": "cd client && npm build",
"client:lint": "cd client && npm run lint",
"client:test": "cd client && npm test",
"test": "npm run client:test"
},
"repository": {
"type": "git",
"url": "git+https://github.com/sundowndev/HETICvsEEMI.git"
},
"keywords": [],
"author": "",
"license": "ISC",
"bugs": {
"url": "https://github.com/sundowndev/HETICvsEEMI/issues"
},
"homepage": "https://github.com/sundowndev/HETICvsEEMI#readme",
"devDependencies": {
"lerna": "^3.16.4"
}
}

12
server/Dockerfile Normal file
View File

@ -0,0 +1,12 @@
FROM node:8
WORKDIR /api
COPY ./package-lock.json .
COPY ./package.json .
COPY ./server ./server
# Build
RUN npm install --prefix /api
EXPOSE 3000

View File

@ -1,8 +1,20 @@
{ {
"questions": [ "questions": [
{ {
"q": "Vous travaillez sur votre projet de visualisation de données durant la pause. Vous voudriez pouvoir y passer plus de temps, mais votre cours de team-building va commencer.", "text": "Vous travaillez sur votre projet de visualisation de données durant la pause. Vous voudriez pouvoir y passer plus de temps, mais votre cours de team-building va commencer.",
"a": 1 "answer": 1
},
{
"text": "Le BDE de l'école organise une soirée de ouf. Ça se passe mercredi soir.",
"answer": 1
},
{
"text": "Vous sortez d'un week-end d'intégration aux Bahamas, après 6 heures de vol. Nico vous propose un petit \"after-WEI\" dans un bar proche de l'école.",
"answer": 2
},
{
"text": "Vous êtes en retard sur vos cours, vous vous demandez comment vous allez passer le prochain partiel. Soudain, votre intervenant vous propose de venir dîner chez lui pour en discuter.",
"answer": 1
} }
] ]
} }

View File

@ -1,3 +1,3 @@
{ {
"schools": [{ "id": 1, "name": "Hetic" }, { "id": 2, "name": "EEMI" }] "schools": [{ "id": 1, "name": "HÉTIC" }, { "id": 2, "name": "EEMI" }]
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "server", "name": "server",
"private": true, "private": false,
"version": "1.0.0", "version": "1.0.0",
"description": "", "description": "",
"main": "index.js", "main": "index.js",