Merge pull request 'feature/get-data-from-backend' (#6) from feature/get-data-from-backend into develop-candymat
Reviewed-on: Netzbegruenung/candymat-user-app#6
This commit is contained in:
commit
f239bec4ff
|
@ -1,7 +1,8 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
root: true,
|
root: true,
|
||||||
env: {
|
env: {
|
||||||
node: true
|
node: true,
|
||||||
|
jest: true
|
||||||
},
|
},
|
||||||
'extends': [
|
'extends': [
|
||||||
'plugin:vue/recommended',
|
'plugin:vue/recommended',
|
||||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -23,3 +23,6 @@ yarn-error.log*
|
||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw*
|
*.sw*
|
||||||
|
|
||||||
|
# GraphQl plugin related
|
||||||
|
postgraphile-schema.graphql
|
||||||
|
|
15
.graphqlconfig
Normal file
15
.graphqlconfig
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"name": "Postgraphile Schema",
|
||||||
|
"schemaPath": "postgraphile-schema.graphql",
|
||||||
|
"extensions": {
|
||||||
|
"endpoints": {
|
||||||
|
"Remote SWAPI GraphQL Endpoint": {
|
||||||
|
"url": "http://localhost:5433/graphql",
|
||||||
|
"headers": {
|
||||||
|
"user-agent": "JS GraphQL"
|
||||||
|
},
|
||||||
|
"introspect": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
19
README.md
19
README.md
|
@ -1,6 +1,6 @@
|
||||||
# CANDYMAT
|
# CANDYMAT
|
||||||
|
|
||||||
A Vue.js powered, progressive web voting application for upcoming internal elections of Bündnis90/Die Grünen. Candymat is hosted as a service of netzbegruenung e.V.
|
A Vue.js powered, progressive web voting application for upcoming internal elections of Bündnis90/Die Grünen. Candymat is hosted as a service of netzbegruenung e.V.
|
||||||
|
|
||||||
## Calculation Model
|
## Calculation Model
|
||||||
|
|
||||||
|
@ -16,9 +16,24 @@ This is a Vue.js progressive web application, developed with [`@vue/cli`](https:
|
||||||
| `npm run serve` | Serve with hot reload at localhost:8080 |
|
| `npm run serve` | Serve with hot reload at localhost:8080 |
|
||||||
| `npm run build` | Build for production with minification |
|
| `npm run build` | Build for production with minification |
|
||||||
| `npm run test:unit` | Run all unit tests |
|
| `npm run test:unit` | Run all unit tests |
|
||||||
| `npm run lint` | Runs `standard` over all `.js` and `.vue` files |
|
| `npm run lint` | Runs `standard` over all `.js` and `.vue` files and fixes problems |
|
||||||
| `npm run svg` | Creates all SVG files used in the application |
|
| `npm run svg` | Creates all SVG files used in the application |
|
||||||
| `npm run admin` | Creates `config.yml` for Netlify CMS admin UI |
|
| `npm run admin` | Creates `config.yml` for Netlify CMS admin UI |
|
||||||
|
|
||||||
|
### Working with GraphQl backend
|
||||||
|
|
||||||
|
As a connector to the backend, `apollo-vue` is used.
|
||||||
|
Queries are written as `gql` strings.
|
||||||
|
To have schema hints etc, there is a `.graphqlconfig` file which should help dedicated IDE plugins
|
||||||
|
to infer the GraphQl schema directly from the (running) backend
|
||||||
|
(see main project for more information on how a "running backend" is achieved).
|
||||||
|
For example, the Intellij JS GraphQL plugin will automatically ask to download the schema definition.
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
* To keep the diff to the original euromat source as small as possible certain variables follow a naming convention
|
||||||
|
which may seem weird at first.
|
||||||
|
These include
|
||||||
|
* `party` (better description would be `person`)
|
||||||
|
|
||||||
## Props
|
## Props
|
||||||
This user app is based on source code of EUROMAT targeted at european elections.
|
This user app is based on source code of EUROMAT targeted at european elections.
|
||||||
|
|
|
@ -20,7 +20,7 @@ module.exports = {
|
||||||
'jest-serializer-vue'
|
'jest-serializer-vue'
|
||||||
],
|
],
|
||||||
testMatch: [
|
testMatch: [
|
||||||
'**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)'
|
'**/*.test.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)'
|
||||||
],
|
],
|
||||||
testURL: 'http://localhost/'
|
testURL: 'http://localhost/'
|
||||||
}
|
}
|
||||||
|
|
849
package-lock.json
generated
849
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -5,12 +5,15 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"serve": "npm run svg && vue-cli-service serve",
|
"serve": "npm run svg && vue-cli-service serve",
|
||||||
"build": "npm run svg && npm run admin && npm run data && vue-cli-service build",
|
"build": "npm run svg && npm run admin && npm run data && vue-cli-service build",
|
||||||
"lint": "vue-cli-service lint",
|
"lint": "vue-cli-service lint --fix",
|
||||||
"test:unit": "vue-cli-service test:unit",
|
"test:unit": "vue-cli-service test:unit",
|
||||||
"svg": "vsvg -s ./src/assets/svg -t ./src/assets/icons",
|
"svg": "vsvg -s ./src/assets/svg -t ./src/assets/icons",
|
||||||
"admin": "node bin/admin-yml"
|
"admin": "node bin/admin-yml"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"apollo-boost": "^0.4.9",
|
||||||
|
"graphql": "^15.1.0",
|
||||||
|
"graphql-tag": "^2.10.3",
|
||||||
"lint-staged": "^8.1.5",
|
"lint-staged": "^8.1.5",
|
||||||
"register-service-worker": "^1.6.2",
|
"register-service-worker": "^1.6.2",
|
||||||
"stylelint": "^10.0.0",
|
"stylelint": "^10.0.0",
|
||||||
|
@ -18,6 +21,7 @@
|
||||||
"stylelint-processor-html": "^1.0.0",
|
"stylelint-processor-html": "^1.0.0",
|
||||||
"stylelint-webpack-plugin": "^0.10.5",
|
"stylelint-webpack-plugin": "^0.10.5",
|
||||||
"vue": "^2.6.6",
|
"vue": "^2.6.6",
|
||||||
|
"vue-apollo": "^3.0.3",
|
||||||
"vue-feather-icons": "^4.10.0",
|
"vue-feather-icons": "^4.10.0",
|
||||||
"vue-i18n": "^8.10.0",
|
"vue-i18n": "^8.10.0",
|
||||||
"vue-markdown": "^2.2.4",
|
"vue-markdown": "^2.2.4",
|
||||||
|
@ -41,7 +45,7 @@
|
||||||
"eslint-plugin-vue": "^5.0.0",
|
"eslint-plugin-vue": "^5.0.0",
|
||||||
"husky": "^1.3.1",
|
"husky": "^1.3.1",
|
||||||
"js-yaml": "^3.13.1",
|
"js-yaml": "^3.13.1",
|
||||||
"node-sass": "^4.13.1",
|
"node-sass": "^4.14.1",
|
||||||
"normalize.css": "^8.0.1",
|
"normalize.css": "^8.0.1",
|
||||||
"ora": "^3.4.0",
|
"ora": "^3.4.0",
|
||||||
"sass-loader": "^7.2.0",
|
"sass-loader": "^7.2.0",
|
||||||
|
|
|
@ -37,8 +37,8 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { theses } from '@/data'
|
|
||||||
import { getTranslatedUrl } from '@/i18n/helper'
|
import { getTranslatedUrl } from '@/i18n/helper'
|
||||||
|
import { apolloThesesQuery, apolloThesesUpdate } from '@/app/euromat/graphqlQueries'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Emphasis',
|
name: 'Emphasis',
|
||||||
|
@ -52,11 +52,18 @@
|
||||||
|
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
theses,
|
theses: [],
|
||||||
emphasized: []
|
emphasized: []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
apollo: {
|
||||||
|
theses: {
|
||||||
|
query: apolloThesesQuery,
|
||||||
|
update: apolloThesesUpdate
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
isEmbedded () {
|
isEmbedded () {
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -11,17 +11,10 @@
|
||||||
|
|
||||||
<ul class="party-results">
|
<ul class="party-results">
|
||||||
<li v-for="party of parties" :key="party.token">
|
<li v-for="party of parties" :key="party.token">
|
||||||
<router-link :to="{ path: getPartyPath(party.token) }">
|
<router-link :to="{ path: getPartyPath(party.id) }">
|
||||||
<div class="result-party-info">
|
<div class="result-party-info">
|
||||||
<div class="result-party-logo">
|
<div class="result-party-logo">
|
||||||
<img
|
<span>{{ party.token }}</span>
|
||||||
v-if="hasPartyLogo(party.token)"
|
|
||||||
:src="getPartyLogo(party.token)"
|
|
||||||
width="50"
|
|
||||||
height="50"
|
|
||||||
:alt="party.token"
|
|
||||||
>
|
|
||||||
<span v-else>{{ party.token }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2>{{ getScorePercentage(party.score) }}%</h2>
|
<h2>{{ getScorePercentage(party.score) }}%</h2>
|
||||||
|
@ -32,31 +25,13 @@
|
||||||
<v-progress
|
<v-progress
|
||||||
class="result-percentage"
|
class="result-percentage"
|
||||||
:value="party.score"
|
:value="party.score"
|
||||||
:max="totalScoredPoints"
|
:max="totalMaxPoints"
|
||||||
/>
|
/>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
<div v-if="party.nationalParty" class="party-results-national">
|
<div class="party-results-national">
|
||||||
<feather-corner-down-right />
|
|
||||||
<span>
|
<span>
|
||||||
{{ $t('results.nationalParty') }}
|
{{ party.name }}
|
||||||
<a
|
|
||||||
class="party-results-national-logo"
|
|
||||||
:href="party.nationalParty.program"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
>
|
|
||||||
<div v-if="hasPartyLogo(party.nationalParty.token)">
|
|
||||||
<img
|
|
||||||
:src="getPartyLogo(party.nationalParty.token)"
|
|
||||||
:alt="party.nationalParty.name"
|
|
||||||
:title="party.nationalParty.name"
|
|
||||||
width="40"
|
|
||||||
height="40"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<span v-else>{{ party.nationalParty.token }}</span>
|
|
||||||
</a>
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
@ -79,36 +54,16 @@
|
||||||
<feather-rotate-cw />
|
<feather-rotate-cw />
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="results-affiliation">
|
|
||||||
<a
|
|
||||||
href="https://www.talkingeurope.com/"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
:src="talkingEuropeBanner"
|
|
||||||
title="Talking Europe"
|
|
||||||
alt="Talking Europe Banner"
|
|
||||||
>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { IPDATA_URL } from '@/config/api'
|
import { getTranslatedUrl } from '@/i18n/helper'
|
||||||
import { getTranslatedUrl, getUserLanguage } from '@/i18n/helper'
|
import { getPartiesWithScores, getTotalMaxPoints } from '@/app/euromat/scoring'
|
||||||
import {
|
import {
|
||||||
MAX_POINTS,
|
apolloPersonsForResultsQuery,
|
||||||
BASE_POINTS,
|
apolloPersonsForResultsUpdate
|
||||||
MIN_POINTS,
|
} from '@/app/euromat/graphqlQueries'
|
||||||
EMPHASIS_POINTS,
|
|
||||||
getScoringGrid
|
|
||||||
} from '@/app/euromat/scoring'
|
|
||||||
import { parties } from '@/data'
|
|
||||||
|
|
||||||
const addUp = (a, b) => a + b
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Results',
|
name: 'Results',
|
||||||
|
@ -117,20 +72,15 @@
|
||||||
'feather-zoom-in': () =>
|
'feather-zoom-in': () =>
|
||||||
import('vue-feather-icons/icons/ZoomInIcon' /* webpackChunkName: "icons" */),
|
import('vue-feather-icons/icons/ZoomInIcon' /* webpackChunkName: "icons" */),
|
||||||
'feather-rotate-cw': () =>
|
'feather-rotate-cw': () =>
|
||||||
import('vue-feather-icons/icons/RotateCwIcon' /* webpackChunkName: "icons" */),
|
import('vue-feather-icons/icons/RotateCwIcon' /* webpackChunkName: "icons" */)
|
||||||
'feather-corner-down-right': () =>
|
|
||||||
import('vue-feather-icons/icons/CornerDownRightIcon' /* webpackChunkName: "icons" */)
|
|
||||||
},
|
},
|
||||||
|
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
userCountry: getUserLanguage().country,
|
|
||||||
scoringGrid: [],
|
scoringGrid: [],
|
||||||
answers: [],
|
|
||||||
emphasized: [],
|
|
||||||
scores: [],
|
scores: [],
|
||||||
parties,
|
parties: [],
|
||||||
totalScoredPoints: 0
|
totalMaxPoints: 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -143,14 +93,6 @@
|
||||||
this.$route.query.embedded &&
|
this.$route.query.embedded &&
|
||||||
this.$route.query.embedded === 'iframe'
|
this.$route.query.embedded === 'iframe'
|
||||||
)
|
)
|
||||||
},
|
|
||||||
talkingEuropeBanner () {
|
|
||||||
try {
|
|
||||||
return require(`@/assets/talkingeurope/talkingeurope-${this.$i18n.locale}.png`)
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('TalkingEurope image not found, defaulting to "en". ', e)
|
|
||||||
return require(`@/assets/talkingeurope/talkingeurope-en.png`)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -167,126 +109,30 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!emphasized) {
|
if (!emphasized) {
|
||||||
this.$router.push({ path: getTranslatedUrl('theses') })
|
await this.$router.push({ path: getTranslatedUrl('theses') })
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const apolloResponse = await this.$apollo.query({ query: apolloPersonsForResultsQuery })
|
||||||
const ipResponse = await fetch(IPDATA_URL)
|
const parties = apolloPersonsForResultsUpdate(apolloResponse.data)
|
||||||
const ipData = await ipResponse.json()
|
|
||||||
if (ipData.country_code) {
|
|
||||||
this.userCountry = ipData.country_code.toLowerCase()
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Unable to fetch geo location:', error)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.emphasized = emphasized
|
const partiesWithScores = getPartiesWithScores(answers, emphasized, parties)
|
||||||
this.answers = answers
|
this.parties = partiesWithScores.map(party => ({
|
||||||
|
id: party.id,
|
||||||
this.scoringGrid = getScoringGrid(this.answers, this.emphasized)
|
token: party.token,
|
||||||
this.scores = this.getScorePoints(this.scoringGrid)
|
score: party.score,
|
||||||
this.parties = this.parties
|
name: party.name
|
||||||
.map(this.getScorePerParty)
|
}))
|
||||||
.sort((a, b) => a.score - b.score)
|
.sort((a, b) => a.score - b.score)
|
||||||
.reverse()
|
.reverse()
|
||||||
this.totalScoredPoints = this.scores
|
this.totalMaxPoints = getTotalMaxPoints(answers, emphasized)
|
||||||
.map(s => s.highestScore)
|
|
||||||
.reduce(addUp, 0)
|
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
getPartyPath (token) {
|
getPartyPath (partyId) {
|
||||||
return `${getTranslatedUrl('party')}/${token.toLowerCase()}`
|
return `${getTranslatedUrl('party')}/${partyId}`
|
||||||
},
|
|
||||||
getPartyLogo (token) {
|
|
||||||
try {
|
|
||||||
return require(`@/assets/svg/${token.toLowerCase().replace(/\s/g, '-')}-logo.svg`)
|
|
||||||
} catch (e) {
|
|
||||||
try {
|
|
||||||
return require(`@/assets/${token.toLowerCase().replace(/\s/g, '-')}-logo.png`)
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(`No logo found for party "${token}", falling back to initials.`, error.message)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
hasPartyLogo (token) {
|
|
||||||
try {
|
|
||||||
require(`@/assets/svg/${token.toLowerCase().replace(/\s/g, '-')}-logo.svg`)
|
|
||||||
return true
|
|
||||||
} catch (e) {
|
|
||||||
try {
|
|
||||||
require(`@/assets/${token.toLowerCase().replace(/\s/g, '-')}-logo.png`)
|
|
||||||
return true
|
|
||||||
} catch (error) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
getScorePercentage (score) {
|
getScorePercentage (score) {
|
||||||
return (score / this.totalScoredPoints * 100).toFixed(2)
|
return (score / this.totalMaxPoints * 100).toFixed(2)
|
||||||
},
|
|
||||||
evalPoints (party, user, emphasis) {
|
|
||||||
let score = 0
|
|
||||||
|
|
||||||
if (user.position === party.position) {
|
|
||||||
score = MAX_POINTS
|
|
||||||
} else if (
|
|
||||||
(user.position === 'positive' && party.position === 'neutral') ||
|
|
||||||
(user.position === 'neutral' && party.position === 'positive') ||
|
|
||||||
(user.position === 'negative' && party.position === 'neutral')
|
|
||||||
) {
|
|
||||||
score = BASE_POINTS
|
|
||||||
} else if (
|
|
||||||
(user.position === 'positive' && party.position === 'negative') ||
|
|
||||||
(user.position === 'neutral' && party.position === 'negative') ||
|
|
||||||
(user.position === 'negative' && party.position === 'positive')
|
|
||||||
) {
|
|
||||||
score = MIN_POINTS
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
party: party.party,
|
|
||||||
score: emphasis ? score * EMPHASIS_POINTS : score
|
|
||||||
}
|
|
||||||
},
|
|
||||||
getHighestScore (scores) {
|
|
||||||
const highestScore = Math.max(...scores.map(s => s.score))
|
|
||||||
|
|
||||||
if (!highestScore) {
|
|
||||||
return MIN_POINTS
|
|
||||||
}
|
|
||||||
|
|
||||||
return highestScore === 1
|
|
||||||
? MAX_POINTS
|
|
||||||
: highestScore
|
|
||||||
},
|
|
||||||
getScorePoints (grid) {
|
|
||||||
// 1. Iterate over scoringGrid
|
|
||||||
// 2. Get user and party positions of each thesis
|
|
||||||
// 3. Evaluate points based on calculation model for each party
|
|
||||||
// 4. Count the highest score per thesis
|
|
||||||
// 5. Return a new object for each thesis row with results
|
|
||||||
return grid.map(row => {
|
|
||||||
const partiesFromRow = row.positions.filter(p => p.type === 'party')
|
|
||||||
const user = row.positions[row.positions.length - 1]
|
|
||||||
const scores = partiesFromRow.map(party => this.evalPoints(party, user, row.emphasis))
|
|
||||||
const highestScore = this.getHighestScore(scores)
|
|
||||||
return {
|
|
||||||
thesis: row.thesis,
|
|
||||||
highestScore,
|
|
||||||
scores
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
getScorePerParty (party) {
|
|
||||||
return {
|
|
||||||
token: party.token,
|
|
||||||
score: this.scores
|
|
||||||
.map(t => t.scores.find(s => s.party === party.id).score)
|
|
||||||
.reduce(addUp, 0),
|
|
||||||
nationalParty: party['national_parties'][this.userCountry]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
<template>
|
<template>
|
||||||
<section class="theses">
|
<section v-if="thesesCount > 0 && theses.length > 0" class="theses">
|
||||||
<div class="header-progress">
|
<div class="header-progress">
|
||||||
<div>
|
<div>
|
||||||
<span class="progress-current">{{ currentThesisStep }}</span>
|
<span class="progress-current">{{ currentThesisStep }}</span>
|
||||||
<span>/{{ thesesCount }}</span>
|
<span>/{{ thesesCount }}</span>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
:disabled="currentThesis === 0"
|
:disabled="currentThesis === 1"
|
||||||
class="btn-dark btn-small"
|
class="btn-dark btn-small"
|
||||||
type="button"
|
type="button"
|
||||||
@click="goBack"
|
@click="goBack"
|
||||||
|
@ -23,9 +23,9 @@
|
||||||
|
|
||||||
<div class="theses-controls">
|
<div class="theses-controls">
|
||||||
<ul class="theses-btns">
|
<ul class="theses-btns">
|
||||||
<li v-for="option in options" :key="option.label">
|
<li v-for="possiblePosition in possiblePositions" :key="possiblePosition.label">
|
||||||
<button type="button" @click="submitAnswer(option, $event)">
|
<button type="button" @click="submitAnswer(possiblePosition, $event)">
|
||||||
{{ option.label }} <component :is="'feather-' + option.icon" />
|
{{ possiblePosition.label }} <component :is="'feather-' + possiblePosition.icon" />
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -35,17 +35,27 @@
|
||||||
type="button"
|
type="button"
|
||||||
@click="submitAnswer(optionSkip)"
|
@click="submitAnswer(optionSkip)"
|
||||||
>
|
>
|
||||||
{{ optionSkip.label }} <feather-corner-up-right />
|
{{ optionSkip.label }}
|
||||||
|
<feather-corner-up-right />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
<section v-else>
|
||||||
|
<span>Loading...</span>
|
||||||
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { options, theses } from '@/data'
|
import possiblePositions from '@/app/euromat/possiblePositions'
|
||||||
import { getTranslatedUrl } from '@/i18n/helper'
|
import { getTranslatedUrl } from '@/i18n/helper'
|
||||||
|
import {
|
||||||
|
apolloThesesCountQuery,
|
||||||
|
apolloThesesCountUpdate,
|
||||||
|
apolloThesesQuery,
|
||||||
|
apolloThesesUpdate
|
||||||
|
} from '@/app/euromat/graphqlQueries'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'EuroMat',
|
name: 'EuroMat',
|
||||||
|
@ -63,12 +73,24 @@
|
||||||
|
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
currentThesis: 0,
|
currentThesis: 1,
|
||||||
thesesCount: theses.length,
|
theses: [],
|
||||||
|
thesesCount: 0,
|
||||||
answers: []
|
answers: []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
apollo: {
|
||||||
|
theses: {
|
||||||
|
query: apolloThesesQuery,
|
||||||
|
update: apolloThesesUpdate
|
||||||
|
},
|
||||||
|
thesesCount: {
|
||||||
|
query: apolloThesesCountQuery,
|
||||||
|
update: apolloThesesCountUpdate
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
isEmbedded () {
|
isEmbedded () {
|
||||||
return (
|
return (
|
||||||
|
@ -77,22 +99,22 @@
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
currentThesisStep () {
|
currentThesisStep () {
|
||||||
return this.currentThesis + 1
|
return this.currentThesis
|
||||||
},
|
},
|
||||||
thesisTitle () {
|
thesisTitle () {
|
||||||
if (this.currentThesis === this.thesesCount) {
|
if (this.currentThesis > this.thesesCount) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
return this.getThesis(this.currentThesis).thesis[this.$i18n.locale]
|
return this.getThesis(this.currentThesis).thesis[this.$i18n.locale]
|
||||||
},
|
},
|
||||||
thesisCategory () {
|
thesisCategory () {
|
||||||
if (this.currentThesis === this.thesesCount) {
|
if (this.currentThesis > this.thesesCount) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
return this.getThesis(this.currentThesis).category[this.$i18n.locale]
|
return this.getThesis(this.currentThesis).category[this.$i18n.locale]
|
||||||
},
|
},
|
||||||
options () {
|
possiblePositions () {
|
||||||
return options.map(option =>
|
return possiblePositions.map(option =>
|
||||||
Object.assign({}, option, {
|
Object.assign({}, option, {
|
||||||
label: this.$t(`theses.${option.position}`),
|
label: this.$t(`theses.${option.position}`),
|
||||||
icon: this.getIconName(option.position)
|
icon: this.getIconName(option.position)
|
||||||
|
@ -100,7 +122,7 @@
|
||||||
.filter(option => option.position !== 'skipped')
|
.filter(option => option.position !== 'skipped')
|
||||||
},
|
},
|
||||||
optionSkip () {
|
optionSkip () {
|
||||||
const skipped = options.find(option => option.position === 'skipped')
|
const skipped = possiblePositions.find(option => option.position === 'skipped')
|
||||||
return Object.assign({}, skipped, {
|
return Object.assign({}, skipped, {
|
||||||
label: this.$t('theses.skipped')
|
label: this.$t('theses.skipped')
|
||||||
})
|
})
|
||||||
|
@ -118,7 +140,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getThesis (id) {
|
getThesis (id) {
|
||||||
return theses.find(t => t.id === id)
|
return this.theses.find(t => t.id === id)
|
||||||
},
|
},
|
||||||
goBack () {
|
goBack () {
|
||||||
const thesis = this.getThesis(this.currentThesis)
|
const thesis = this.getThesis(this.currentThesis)
|
||||||
|
@ -138,7 +160,7 @@
|
||||||
event && event.target.blur()
|
event && event.target.blur()
|
||||||
window.scrollTo(0, 0)
|
window.scrollTo(0, 0)
|
||||||
|
|
||||||
if (this.currentThesis === this.thesesCount) {
|
if (this.currentThesis > this.thesesCount) {
|
||||||
this.forwardToResults()
|
this.forwardToResults()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
115
src/app/euromat/graphqlQueries.js
Normal file
115
src/app/euromat/graphqlQueries.js
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
import gql from 'graphql-tag'
|
||||||
|
import possiblePositions from '@/app/euromat/possiblePositions'
|
||||||
|
|
||||||
|
export function getPositionById (id) {
|
||||||
|
return possiblePositions.find(option => option.id === id).position
|
||||||
|
}
|
||||||
|
|
||||||
|
export const apolloThesesQuery = gql`{
|
||||||
|
allQuestions(orderBy: ID_ASC) {
|
||||||
|
nodes {
|
||||||
|
category: categoryByCategoryId {
|
||||||
|
nodeId
|
||||||
|
title
|
||||||
|
}
|
||||||
|
text
|
||||||
|
id
|
||||||
|
nodeId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
export const apolloThesesUpdate = data => data.allQuestions.nodes.map(node => ({
|
||||||
|
id: node.id,
|
||||||
|
thesis: {
|
||||||
|
de: node.text
|
||||||
|
},
|
||||||
|
category: {
|
||||||
|
de: node.category.title
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
export const apolloThesesCountQuery = gql`{
|
||||||
|
allQuestions {
|
||||||
|
totalCount
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
export const apolloThesesCountUpdate = data => data.allQuestions.totalCount
|
||||||
|
|
||||||
|
export const apolloPersonsForResultsQuery = gql`{
|
||||||
|
allPeople(condition: {role: CANDYMAT_CANDIDATE}) {
|
||||||
|
nodes {
|
||||||
|
nodeId
|
||||||
|
firstName
|
||||||
|
lastName
|
||||||
|
id
|
||||||
|
answersByPersonId {
|
||||||
|
nodes {
|
||||||
|
nodeId
|
||||||
|
position
|
||||||
|
questionId
|
||||||
|
text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
export const apolloPersonsForResultsUpdate = data => data.allPeople.nodes.map(person => ({
|
||||||
|
id: person.id,
|
||||||
|
name: `${person.firstName} ${person.lastName}`,
|
||||||
|
token: person.firstName.charAt(0) + person.lastName.charAt(0),
|
||||||
|
positions: person.answersByPersonId.nodes.map(answer => ({
|
||||||
|
thesis: answer.questionId,
|
||||||
|
position: getPositionById(answer.position),
|
||||||
|
statement: {
|
||||||
|
de: answer.text
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}))
|
||||||
|
|
||||||
|
export const apolloPersonPositionsQuery = gql`
|
||||||
|
query Person($partyId: Int!) {
|
||||||
|
personById(id: $partyId) {
|
||||||
|
nodeId
|
||||||
|
id
|
||||||
|
firstName
|
||||||
|
lastName
|
||||||
|
answersByPersonId {
|
||||||
|
nodes {
|
||||||
|
nodeId
|
||||||
|
position
|
||||||
|
personId
|
||||||
|
text
|
||||||
|
questionByQuestionId {
|
||||||
|
nodeId
|
||||||
|
categoryByCategoryId {
|
||||||
|
nodeId
|
||||||
|
title
|
||||||
|
}
|
||||||
|
text
|
||||||
|
id
|
||||||
|
description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
export const apolloPersonPositionsUpdate = data => ({
|
||||||
|
id: data.personById.id,
|
||||||
|
name: `${data.personById.firstName} ${data.personById.lastName}`,
|
||||||
|
token: data.personById.firstName.charAt(0) + data.personById.lastName.charAt(0),
|
||||||
|
theses: data.personById.answersByPersonId.nodes.map(answer => {
|
||||||
|
const question = answer.questionByQuestionId
|
||||||
|
return {
|
||||||
|
id: question.id,
|
||||||
|
thesis: question.text,
|
||||||
|
category: question.categoryByCategoryId.title,
|
||||||
|
position: getPositionById(answer.position),
|
||||||
|
statement: answer.text,
|
||||||
|
showStatement: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
|
@ -1,4 +1,4 @@
|
||||||
const options = [
|
const possiblePositions = [
|
||||||
{
|
{
|
||||||
'position': 'positive',
|
'position': 'positive',
|
||||||
'id': 0
|
'id': 0
|
||||||
|
@ -16,5 +16,4 @@ const options = [
|
||||||
'id': 3
|
'id': 3
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
export default possiblePositions
|
||||||
export default options
|
|
|
@ -1,21 +1,60 @@
|
||||||
import { parties } from '@/data'
|
|
||||||
|
|
||||||
export const MAX_POINTS = 2
|
export const MAX_POINTS = 2
|
||||||
export const BASE_POINTS = 1
|
export const BASE_POINTS = 1
|
||||||
export const MIN_POINTS = 0
|
export const MIN_POINTS = 0
|
||||||
export const EMPHASIS_POINTS = 2
|
export const EMPHASIS_POINTS = 2
|
||||||
|
|
||||||
export function getPartyPositions (thesis) {
|
export function getPartiesWithScores (answers, emphasized, partiesPositions) {
|
||||||
return parties.map(party => {
|
const scorePointsGrid = getScorePointsGrid(answers, emphasized, partiesPositions)
|
||||||
const position = party.positions.find(p => p.thesis === thesis)
|
|
||||||
|
return partiesPositions.map(party => ({
|
||||||
|
...party,
|
||||||
|
score: getTotalScorePerParty(party, scorePointsGrid)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTotalMaxPoints (userAnswers, userEmphasized) {
|
||||||
|
return userAnswers.map(answer => {
|
||||||
|
const emphasis = userEmphasized.filter(e => e.thesis === answer.thesis).length >= 1
|
||||||
|
return getMaxScorePerThesis(answer.position, emphasis)
|
||||||
|
}).reduce((total, maxScorePerRow) => total + maxScorePerRow)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getScorePointsGrid (userAnswers, userEmphasized, partiesPositions) {
|
||||||
|
// 1. Iterate over scoringGrid
|
||||||
|
// 2. Get user and party positions of each thesis
|
||||||
|
// 3. Evaluate points based on calculation model for each party
|
||||||
|
// 4. Get the maximum score per thesis
|
||||||
|
// 5. Return a new object for each thesis row with results
|
||||||
|
const scoringGrid = getScoringGrid(userAnswers, userEmphasized, partiesPositions)
|
||||||
|
|
||||||
|
return scoringGrid.map(row => {
|
||||||
|
const partiesFromRow = row.positions.filter(p => p.type === 'party')
|
||||||
|
const userPosition = getUserPosition(row)
|
||||||
|
const scores = partiesFromRow.map(party => ({
|
||||||
|
party: party.party,
|
||||||
|
score: evalPointsPerThesisPerParty(party.position, userPosition, row.emphasis)
|
||||||
|
}))
|
||||||
return {
|
return {
|
||||||
type: 'party',
|
thesis: row.thesis,
|
||||||
party: party.id,
|
scores
|
||||||
position: (position && position.position) || {}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getTotalScorePerParty (party, scorePointsGrid) {
|
||||||
|
return scorePointsGrid
|
||||||
|
.map(thesis => thesis.scores.find(scores => scores.party === party.id).score)
|
||||||
|
.reduce((total, score) => total + score, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMaxScorePerThesis (userPosition, emphasis) {
|
||||||
|
return userPosition === 'skipped' ? MIN_POINTS : emphasis ? MAX_POINTS * EMPHASIS_POINTS : MAX_POINTS
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUserPosition (row) {
|
||||||
|
return row.positions.find(p => p.type === 'user').position
|
||||||
|
}
|
||||||
|
|
||||||
// Grid example:
|
// Grid example:
|
||||||
// [
|
// [
|
||||||
// {
|
// {
|
||||||
|
@ -33,15 +72,48 @@ export function getPartyPositions (thesis) {
|
||||||
// },
|
// },
|
||||||
// ...
|
// ...
|
||||||
// ]
|
// ]
|
||||||
export function getScoringGrid (userAnswers, emphasizedTheses) {
|
function getScoringGrid (userAnswers, emphasizedTheses, parties) {
|
||||||
return userAnswers.map(answer => (
|
return userAnswers.map(answer => (
|
||||||
{
|
{
|
||||||
thesis: answer.thesis,
|
thesis: answer.thesis,
|
||||||
emphasis: emphasizedTheses.filter(e => e.thesis === answer.thesis).length >= 1,
|
emphasis: emphasizedTheses.filter(e => e.thesis === answer.thesis).length >= 1,
|
||||||
positions: [
|
positions: [
|
||||||
...getPartyPositions(answer.thesis),
|
...getPartyPositions(answer.thesis, parties),
|
||||||
...[{ type: 'user', position: answer.position }]
|
...[{ type: 'user', position: answer.position }]
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getPartyPositions (thesis, parties) {
|
||||||
|
return parties.map(party => {
|
||||||
|
const position = party.positions.find(p => p.thesis === thesis)
|
||||||
|
return {
|
||||||
|
type: 'party',
|
||||||
|
party: party.id,
|
||||||
|
position: (position && position.position) || {}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function evalPointsPerThesisPerParty (partyPosition, userPosition, emphasis) {
|
||||||
|
let score = 0
|
||||||
|
|
||||||
|
if (userPosition === partyPosition) {
|
||||||
|
score = MAX_POINTS
|
||||||
|
} else if (
|
||||||
|
(userPosition === 'positive' && partyPosition === 'neutral') ||
|
||||||
|
(userPosition === 'neutral' && partyPosition === 'positive') ||
|
||||||
|
(userPosition === 'neutral' && partyPosition === 'negative') ||
|
||||||
|
(userPosition === 'negative' && partyPosition === 'neutral')
|
||||||
|
) {
|
||||||
|
score = BASE_POINTS
|
||||||
|
} else if (
|
||||||
|
(userPosition === 'positive' && partyPosition === 'negative') ||
|
||||||
|
(userPosition === 'negative' && partyPosition === 'positive')
|
||||||
|
) {
|
||||||
|
score = MIN_POINTS
|
||||||
|
}
|
||||||
|
|
||||||
|
return emphasis ? score * EMPHASIS_POINTS : score
|
||||||
|
}
|
||||||
|
|
109
src/app/euromat/scoring.test.js
Normal file
109
src/app/euromat/scoring.test.js
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
import { evalPointsPerThesisPerParty, getPartiesWithScores, getTotalMaxPoints } from '@/app/euromat/scoring'
|
||||||
|
|
||||||
|
const positionDef = ['positive', 'neutral', 'negative', 'skipped']
|
||||||
|
// See offficial Rechenmodell of bpb from 2019
|
||||||
|
const rechenmodellGrid = [
|
||||||
|
[0, 0, 0, 0, 0, false],
|
||||||
|
[0, 0, 2, 0, 1, false],
|
||||||
|
[0, 2, 2, 0, 0, false],
|
||||||
|
[0, 0, 0, 0, 2, false],
|
||||||
|
[1, 1, 0, 2, 2, true],
|
||||||
|
[0, 0, 2, 0, 0, false],
|
||||||
|
[0, 2, 2, 1, 0, false],
|
||||||
|
[0, 0, 0, 2, 2, false],
|
||||||
|
[2, 0, 2, 2, 1, false],
|
||||||
|
[1, 2, 2, 2, 1, false],
|
||||||
|
[2, 2, 0, 0, 2, false],
|
||||||
|
[0, 2, 2, 2, 2, false],
|
||||||
|
[0, 2, 0, 0, 3, false],
|
||||||
|
[0, 2, 1, 2, 2, false],
|
||||||
|
[2, 0, 0, 1, 2, false],
|
||||||
|
[2, 0, 0, 0, 2, true],
|
||||||
|
[2, 0, 0, 2, 3, false],
|
||||||
|
[0, 0, 0, 0, 2, false],
|
||||||
|
[2, 0, 0, 0, 1, false],
|
||||||
|
[2, 0, 0, 0, 1, false],
|
||||||
|
[0, 0, 2, 2, 2, true],
|
||||||
|
[2, 2, 1, 2, 2, false],
|
||||||
|
[2, 2, 0, 2, 2, false],
|
||||||
|
[2, 2, 2, 0, 2, false],
|
||||||
|
[0, 2, 0, 2, 1, false],
|
||||||
|
[1, 2, 2, 0, 0, false],
|
||||||
|
[2, 0, 2, 0, 0, false],
|
||||||
|
[2, 0, 0, 2, 2, false],
|
||||||
|
[0, 2, 0, 2, 1, false],
|
||||||
|
[1, 2, 0, 2, 2, false],
|
||||||
|
[0, 1, 2, 0, 0, false],
|
||||||
|
[1, 2, 2, 0, 1, false],
|
||||||
|
[2, 0, 2, 2, 1, false],
|
||||||
|
[0, 2, 2, 2, 2, false],
|
||||||
|
[0, 2, 0, 0, 0, false],
|
||||||
|
[0, 2, 0, 0, 1, false],
|
||||||
|
[2, 2, 2, 2, 0, false],
|
||||||
|
[2, 0, 0, 2, 0, false]
|
||||||
|
]
|
||||||
|
|
||||||
|
const rechenmodellExpectedTotalScorePerParty = [44, 37, 28, 50]
|
||||||
|
const rechenmodellExpectedMaxScore = 78
|
||||||
|
|
||||||
|
const testParties = [1, 2, 3, 4].map(partyId => ({
|
||||||
|
id: partyId,
|
||||||
|
token: 'idontcare',
|
||||||
|
positions: rechenmodellGrid.map((thesis, index) => ({
|
||||||
|
thesis: index,
|
||||||
|
position: positionDef[thesis[partyId - 1]]
|
||||||
|
}))
|
||||||
|
}))
|
||||||
|
|
||||||
|
const testAnswers = rechenmodellGrid.map((thesis, index) => ({
|
||||||
|
position: positionDef[thesis[4]],
|
||||||
|
thesis: index
|
||||||
|
}))
|
||||||
|
|
||||||
|
const testEmphasis = rechenmodellGrid
|
||||||
|
.map((thesis, index) => ({
|
||||||
|
...thesis,
|
||||||
|
thesis: index
|
||||||
|
}))
|
||||||
|
.filter((thesis, index) => thesis[5])
|
||||||
|
.map(thesis => ({ thesis: thesis.thesis }))
|
||||||
|
|
||||||
|
describe('The getPartiesWithScores function', () => {
|
||||||
|
it('returns the correct total scores according to the Rechenmodell example of bpb', () => {
|
||||||
|
const resultPartiesWithScores = getPartiesWithScores(testAnswers, testEmphasis, testParties)
|
||||||
|
|
||||||
|
expect(resultPartiesWithScores.map(party => party.score)).toEqual(rechenmodellExpectedTotalScorePerParty)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('The getTotalMaxPoints function', () => {
|
||||||
|
it('returns the correct maximum score points according to the Rechenmodell example of bpb', () => {
|
||||||
|
const resultTotalMaxPoints = getTotalMaxPoints(testAnswers, testEmphasis)
|
||||||
|
|
||||||
|
expect(resultTotalMaxPoints).toEqual(rechenmodellExpectedMaxScore)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('The evalPointsPerThesisPerParty fucntion', () => {
|
||||||
|
test.each`
|
||||||
|
partyPosition | userPosition | expectedScore | expectedScoreWithEmphasis
|
||||||
|
${'negative'} | ${'negative'} | ${2} | ${4}
|
||||||
|
${'negative'} | ${'neutral'} | ${1} | ${2}
|
||||||
|
${'negative'} | ${'positive'} | ${0} | ${0}
|
||||||
|
${'neutral'} | ${'negative'} | ${1} | ${2}
|
||||||
|
${'neutral'} | ${'neutral'} | ${2} | ${4}
|
||||||
|
${'neutral'} | ${'positive'} | ${1} | ${2}
|
||||||
|
${'positive'} | ${'negative'} | ${0} | ${0}
|
||||||
|
${'positive'} | ${'neutral'} | ${1} | ${2}
|
||||||
|
${'positive'} | ${'positive'} | ${2} | ${4}
|
||||||
|
${'positive'} | ${'skipped'} | ${0} | ${0}
|
||||||
|
`('returns the correct score (according to the Rechenmodell of the bpb' +
|
||||||
|
' if the party position is $partyPosition and the user\'s position is $userPosition',
|
||||||
|
({ partyPosition, userPosition, expectedScore, expectedScoreWithEmphasis }) => {
|
||||||
|
const resultScore = evalPointsPerThesisPerParty(partyPosition, userPosition, false)
|
||||||
|
const resultScoreWithEmphasis = evalPointsPerThesisPerParty(partyPosition, userPosition, true)
|
||||||
|
|
||||||
|
expect(resultScore).toEqual(expectedScore)
|
||||||
|
expect(resultScoreWithEmphasis).toEqual(expectedScoreWithEmphasis)
|
||||||
|
})
|
||||||
|
})
|
|
@ -1,28 +1,21 @@
|
||||||
<template>
|
<template>
|
||||||
<section>
|
<section>
|
||||||
<header :class="['party-header', { 'no-party-link': !partyProgramLink }]">
|
<header :class="['party-header', { 'no-party-link': !partyProgramLink }]">
|
||||||
<div :class="['party-header-logo', { 'no-logo': !hasPartyLogo }]">
|
<div :class="['party-header-logo', 'no-logo']">
|
||||||
<img
|
<span>
|
||||||
v-if="hasPartyLogo"
|
{{ party.token }}
|
||||||
:src="partyLogo"
|
|
||||||
:width="logoSize"
|
|
||||||
:height="logoSize"
|
|
||||||
:alt="partyName"
|
|
||||||
>
|
|
||||||
<span v-else>
|
|
||||||
{{ partyToken }}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="party-header-info">
|
<div class="party-header-info">
|
||||||
<router-link v-if="!!answers"
|
<router-link v-if="!!answers"
|
||||||
class="btn btn-dark btn-small"
|
class="btn btn-dark btn-small"
|
||||||
:to="{ path: resultsPath }"
|
:to="{ path: resultsPath }"
|
||||||
>
|
>
|
||||||
{{ $t('party.backButtonLabel') }}
|
{{ $t('party.backButtonLabel') }}
|
||||||
<feather-corner-up-left />
|
<feather-corner-up-left />
|
||||||
</router-link>
|
</router-link>
|
||||||
<h1>{{ partyName }}</h1>
|
<h1>{{ party.name }}</h1>
|
||||||
<a
|
<a
|
||||||
v-if="!!partyProgramLink"
|
v-if="!!partyProgramLink"
|
||||||
class="btn"
|
class="btn"
|
||||||
|
@ -38,9 +31,9 @@
|
||||||
<div class="theses-legend">
|
<div class="theses-legend">
|
||||||
<p>{{ $t('party.legendLabel') }}:</p>
|
<p>{{ $t('party.legendLabel') }}:</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li v-for="option in options" :key="option.position">
|
<li v-for="possiblePosition in possiblePositions" :key="possiblePosition.position">
|
||||||
<component :is="'feather-' + positionToIconName(option.position)" />
|
<component :is="'feather-' + positionToIconName(possiblePosition.position)" />
|
||||||
<span>{{ $t(`theses.${option.position}`) }}</span>
|
<span>{{ $t(`theses.${possiblePosition.position}`) }}</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
@ -54,36 +47,36 @@
|
||||||
</h2>
|
</h2>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li v-for="thesis in theses" :key="thesis.id">
|
<li v-for="thesis in party.theses" :key="thesis.id">
|
||||||
<div class="thesis-facts">
|
<div class="thesis-facts">
|
||||||
<div class="list-thesis" @click="toggleStatement(thesis.id)">
|
<div class="list-thesis" @click="toggleStatement(thesis)">
|
||||||
<div class="thesis-subline">
|
<div class="thesis-subline">
|
||||||
<component :is="'feather-' + chevronIcon(thesis.id)" />
|
<component :is="'feather-' + chevronIcon(thesis.showStatement)" />
|
||||||
<span>{{ getCategory(thesis.category) }}</span>
|
<span>{{ thesis.category }}</span>
|
||||||
</div>
|
</div>
|
||||||
<h3>{{ getThesis(thesis.thesis) }}</h3>
|
<h3>{{ thesis.thesis }}</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="statements-party">
|
<div class="statements-party">
|
||||||
<component :is="'feather-' + getPartyPosition(thesis.id)" />
|
<component :is="'feather-' + positionToIconName(thesis.position)" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!!answers" class="statements-user">
|
<div v-if="!!answers" class="statements-user">
|
||||||
<component :is="'feather-' + getUserPosition(thesis.id)" />
|
<component :is="'feather-' + getUserPosition(thesis.id)" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-show="showStatement(thesis.id)" class="thesis-statement">
|
<div v-show="thesis.showStatement" class="thesis-statement">
|
||||||
<p><strong>{{ $t('party.partyAnswer') }}:</strong></p>
|
<p><strong>{{ $t('party.partyAnswer') }}:</strong></p>
|
||||||
<blockquote>
|
<blockquote>
|
||||||
{{ getPartyStatement(thesis.id) }}
|
{{ thesis.statement }}
|
||||||
</blockquote>
|
</blockquote>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<router-link v-if="!!answers"
|
<router-link v-if="!!answers"
|
||||||
class="btn btn-dark btn-small"
|
class="btn btn-dark btn-small"
|
||||||
:to="{ path: resultsPath }"
|
:to="{ path: resultsPath }"
|
||||||
>
|
>
|
||||||
{{ $t('party.backButtonLabel') }}
|
{{ $t('party.backButtonLabel') }}
|
||||||
<feather-corner-up-left />
|
<feather-corner-up-left />
|
||||||
|
@ -92,8 +85,10 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { parties, theses, options } from '@/data'
|
import possiblePositions from '@/app/euromat/possiblePositions'
|
||||||
import { getTranslatedUrl } from '@/i18n/helper'
|
import { getTranslatedUrl } from '@/i18n/helper'
|
||||||
|
import { apolloPersonPositionsQuery, apolloPersonPositionsUpdate } from '@/app/euromat/graphqlQueries'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Party',
|
name: 'Party',
|
||||||
|
|
||||||
|
@ -127,13 +122,27 @@
|
||||||
|
|
||||||
return {
|
return {
|
||||||
logoSize: 60,
|
logoSize: 60,
|
||||||
partyLogo: this.hasPartyLogo && require(`@/assets/svg/${this.$route.params.token}-logo.svg`),
|
partyId: this.$route.params.token,
|
||||||
partyToken: this.$route.params.token.toUpperCase(),
|
party: {
|
||||||
party: parties.find(p => p.token === this.$route.params.token.toUpperCase()),
|
id: this.$route.params.token,
|
||||||
|
token: this.$route.params.token,
|
||||||
|
name: 'Loading ...',
|
||||||
|
theses: []
|
||||||
|
},
|
||||||
answers,
|
answers,
|
||||||
toggles: theses.map(t => ({ id: t.id, show: false })),
|
possiblePositions
|
||||||
theses,
|
}
|
||||||
options
|
},
|
||||||
|
|
||||||
|
apollo: {
|
||||||
|
party: {
|
||||||
|
query: apolloPersonPositionsQuery,
|
||||||
|
variables () {
|
||||||
|
return {
|
||||||
|
partyId: parseInt(this.$route.params.token)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
update: apolloPersonPositionsUpdate
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -141,34 +150,17 @@
|
||||||
resultsPath () {
|
resultsPath () {
|
||||||
return getTranslatedUrl('results', getTranslatedUrl('theses', null, true))
|
return getTranslatedUrl('results', getTranslatedUrl('theses', null, true))
|
||||||
},
|
},
|
||||||
hasPartyLogo () {
|
|
||||||
try {
|
|
||||||
require(`@/assets/svg/${this.$route.params.token}-logo.svg`)
|
|
||||||
return true
|
|
||||||
} catch (error) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
partyName () {
|
|
||||||
return this.party.name[this.$i18n.locale]
|
|
||||||
},
|
|
||||||
partyProgramLink () {
|
partyProgramLink () {
|
||||||
return this.party.program[this.$i18n.locale]
|
return false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
chevronIcon (id) {
|
chevronIcon (showStatement) {
|
||||||
return this.showStatement(id)
|
return showStatement
|
||||||
? 'chevron-up'
|
? 'chevron-up'
|
||||||
: 'chevron-down'
|
: 'chevron-down'
|
||||||
},
|
},
|
||||||
getCategory (category) {
|
|
||||||
return category[this.$i18n.locale]
|
|
||||||
},
|
|
||||||
getThesis (thesis) {
|
|
||||||
return thesis[this.$i18n.locale]
|
|
||||||
},
|
|
||||||
positionToIconName (position) {
|
positionToIconName (position) {
|
||||||
switch (position) {
|
switch (position) {
|
||||||
case 'positive':
|
case 'positive':
|
||||||
|
@ -182,27 +174,13 @@
|
||||||
return 'circle'
|
return 'circle'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getPartyPosition (id) {
|
|
||||||
return this.positionToIconName(
|
|
||||||
this.party.positions.find(p => p.thesis === id).position
|
|
||||||
)
|
|
||||||
},
|
|
||||||
getPartyStatement (id) {
|
|
||||||
return this.party.positions
|
|
||||||
.find(p => p.thesis === id)
|
|
||||||
.statement[this.$i18n.locale]
|
|
||||||
},
|
|
||||||
getUserPosition (id) {
|
getUserPosition (id) {
|
||||||
return this.positionToIconName(
|
return this.positionToIconName(
|
||||||
this.answers.find(a => a.thesis === id).position
|
this.answers.find(a => a.thesis === id).position
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
showStatement (id) {
|
toggleStatement (thesis) {
|
||||||
return this.toggles.find(t => t.id === id).show
|
thesis.showStatement = !thesis.showStatement
|
||||||
},
|
|
||||||
toggleStatement (id) {
|
|
||||||
const statement = this.toggles.find(t => t.id === id)
|
|
||||||
statement.show = !statement.show
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
export const IPDATA_URL = 'https://api.ipdata.co/?api-key=test'
|
|
|
@ -1,7 +1,4 @@
|
||||||
import { loadContent } from '@/helper/content'
|
import { loadContent } from '@/helper/content'
|
||||||
import options from './options'
|
|
||||||
import theses from './theses'
|
|
||||||
import parties from './parties'
|
|
||||||
|
|
||||||
const i18n = loadContent(
|
const i18n = loadContent(
|
||||||
'meta',
|
'meta',
|
||||||
|
@ -9,8 +6,5 @@ const i18n = loadContent(
|
||||||
)
|
)
|
||||||
|
|
||||||
export {
|
export {
|
||||||
options,
|
|
||||||
theses,
|
|
||||||
parties,
|
|
||||||
i18n
|
i18n
|
||||||
}
|
}
|
||||||
|
|
3803
src/data/parties.js
3803
src/data/parties.js
File diff suppressed because it is too large
Load diff
|
@ -1,45 +0,0 @@
|
||||||
const theses = [
|
|
||||||
{
|
|
||||||
'id': 0,
|
|
||||||
'category': {
|
|
||||||
'de': 'Politische Bildung',
|
|
||||||
'en': 'Civic Education',
|
|
||||||
'fr': 'Education civique',
|
|
||||||
'dk': 'Medborgerskab',
|
|
||||||
'si': 'Državljanska vzgoja',
|
|
||||||
'cz': 'Občanské vzdělávání',
|
|
||||||
'pl': 'Edukacja obywatelska'
|
|
||||||
},
|
|
||||||
'thesis': {
|
|
||||||
'de': 'Europapolitische Bildung sollte Teil der Lehrpläne aller Mitgliedsländer sein.',
|
|
||||||
'en': 'European civic education should be part of the school curricula of all member countries.',
|
|
||||||
'fr': 'L’éducation civique européenne devrait faire partie des programmes scolaires de tous les Etats-membres.',
|
|
||||||
'dk': 'Europæisk medborgerskab skal være en del af skolepensum i alle medlemslande.',
|
|
||||||
'si': 'Evropska državljanska vzgoja bi morala biti del šolskega kurikuluma v vseh državah članicah.',
|
|
||||||
'cz': 'Evropské občanské vzdělávání by mělo být součástí školních osnov ve všech členských státech.',
|
|
||||||
'pl': 'Europejska edukacja obywatelska powinna być częścią programów nauczania we wszystkich państwach członkowskich.'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'id': 1,
|
|
||||||
'category': {
|
|
||||||
'de': 'Europäische Armee',
|
|
||||||
'en': 'European Army',
|
|
||||||
'fr': 'Forces armées',
|
|
||||||
'dk': 'Fælles forsvar',
|
|
||||||
'si': 'Evropska vojska',
|
|
||||||
'cz': 'Evropská armáda',
|
|
||||||
'pl': 'Armia europejska'
|
|
||||||
},
|
|
||||||
'thesis': {
|
|
||||||
'de': 'Langfristig sollten die EU-Mitgliedstaaten ihre Streitkräfte zu einer europäischen Armee zusammenschließen.',
|
|
||||||
'en': 'In the long-term EU member states should merge their armed forces to a European army.',
|
|
||||||
'fr': "A terme, les forces armées des pays membres de l'UE devraient fusionner.",
|
|
||||||
'dk': 'På lang sigt skal EU-medlemslandenes forsvar samles i en fælles europæisk hær.',
|
|
||||||
'si': 'Države članice EU bi morale na dolgi rok združiti svoje oborožene sile v skupno evropsko vojsko.',
|
|
||||||
'cz': 'V dlouhodobém horizontu by měly být armády členských států sloučeny do Evropské armády.',
|
|
||||||
'pl': 'W perspektywie długoterminowej państwa członkowskie UE powinny połączyć swoje siły zbrojne w armię europejską.'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
export default theses
|
|
12
src/main.js
12
src/main.js
|
@ -8,14 +8,26 @@ import storage from '@/helper/storage'
|
||||||
|
|
||||||
import '@/registerComponents'
|
import '@/registerComponents'
|
||||||
import '@/registerServiceWorker'
|
import '@/registerServiceWorker'
|
||||||
|
import VueApollo from 'vue-apollo'
|
||||||
|
import ApolloClient from 'apollo-boost'
|
||||||
|
|
||||||
Vue.config.productionTip = false
|
Vue.config.productionTip = false
|
||||||
Vue.use(VueSVGIcon)
|
Vue.use(VueSVGIcon)
|
||||||
Vue.use(storage)
|
Vue.use(storage)
|
||||||
|
Vue.use(VueApollo)
|
||||||
|
|
||||||
|
const apolloClient = new ApolloClient({
|
||||||
|
uri: 'http://localhost:5433/graphql'
|
||||||
|
})
|
||||||
|
|
||||||
|
const apolloProvider = new VueApollo({
|
||||||
|
defaultClient: apolloClient
|
||||||
|
})
|
||||||
|
|
||||||
new Vue({
|
new Vue({
|
||||||
i18n,
|
i18n,
|
||||||
router,
|
router,
|
||||||
|
apolloProvider,
|
||||||
data: {
|
data: {
|
||||||
backupStorage: {
|
backupStorage: {
|
||||||
answers: undefined,
|
answers: undefined,
|
||||||
|
|
Loading…
Reference in a new issue