Merge pull request 'develop-candymat' (#7) from develop-candymat into main
Reviewed-on: #7
This commit is contained in:
commit
dc1602b6e8
|
@ -1,7 +1,8 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
node: true
|
||||
node: true,
|
||||
jest: true
|
||||
},
|
||||
'extends': [
|
||||
'plugin:vue/recommended',
|
||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -23,3 +23,6 @@ yarn-error.log*
|
|||
*.njsproj
|
||||
*.sln
|
||||
*.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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
21
README.md
21
README.md
|
@ -1,6 +1,6 @@
|
|||
# CANDYMAT
|
||||
# KANDIMAT
|
||||
|
||||
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. Kandimat is hosted as a service of netzbegruenung e.V.
|
||||
|
||||
## 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 build` | Build for production with minification |
|
||||
| `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 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
|
||||
This user app is based on source code of EUROMAT targeted at european elections.
|
||||
|
|
|
@ -20,7 +20,7 @@ module.exports = {
|
|||
'jest-serializer-vue'
|
||||
],
|
||||
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/'
|
||||
}
|
||||
|
|
851
package-lock.json
generated
851
package-lock.json
generated
File diff suppressed because it is too large
Load diff
10
package.json
10
package.json
|
@ -1,16 +1,19 @@
|
|||
{
|
||||
"name": "candymat",
|
||||
"name": "kandimat",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "npm run svg && vue-cli-service serve",
|
||||
"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",
|
||||
"svg": "vsvg -s ./src/assets/svg -t ./src/assets/icons",
|
||||
"admin": "node bin/admin-yml"
|
||||
},
|
||||
"dependencies": {
|
||||
"apollo-boost": "^0.4.9",
|
||||
"graphql": "^15.1.0",
|
||||
"graphql-tag": "^2.10.3",
|
||||
"lint-staged": "^8.1.5",
|
||||
"register-service-worker": "^1.6.2",
|
||||
"stylelint": "^10.0.0",
|
||||
|
@ -18,6 +21,7 @@
|
|||
"stylelint-processor-html": "^1.0.0",
|
||||
"stylelint-webpack-plugin": "^0.10.5",
|
||||
"vue": "^2.6.6",
|
||||
"vue-apollo": "^3.0.3",
|
||||
"vue-feather-icons": "^4.10.0",
|
||||
"vue-i18n": "^8.10.0",
|
||||
"vue-markdown": "^2.2.4",
|
||||
|
@ -41,7 +45,7 @@
|
|||
"eslint-plugin-vue": "^5.0.0",
|
||||
"husky": "^1.3.1",
|
||||
"js-yaml": "^3.13.1",
|
||||
"node-sass": "^4.13.1",
|
||||
"node-sass": "^4.14.1",
|
||||
"normalize.css": "^8.0.1",
|
||||
"ora": "^3.4.0",
|
||||
"sass-loader": "^7.2.0",
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Candymat</title>
|
||||
<title>Kandimat</title>
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/img/icons/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/img/icons/favicon_gruene_16x16.png">
|
||||
<!--[if IE]><link rel="shortcut icon" href="/img/favicon.ico"><![endif]-->
|
||||
|
@ -12,22 +12,22 @@
|
|||
<link rel="manifest" href="/manifest.json">
|
||||
<meta name="theme-color" content="#59ae2e">
|
||||
<!-- Open Graph -->
|
||||
<meta property="og:url" content="https://www.candymat.info" />
|
||||
<meta property="og:title" content="Candymat" />
|
||||
<meta property="og:url" content="https://www.kandimat.info" />
|
||||
<meta property="og:title" content="Kandimat" />
|
||||
<meta property="og:locale" content="en_GB" />
|
||||
<meta property="og:locale:alternate" content="de_DE" />
|
||||
<meta property="og:locale:alternate" content="fr_FR" />
|
||||
<meta property="og:locale:alternate" content="cz_CZ" />
|
||||
<meta property="og:locale:alternate" content="si_SI" />
|
||||
<meta property="og:locale:alternate" content="dk_DK" />
|
||||
<meta property="og:description" content="The Candymat is not a regular voting advice application. On the contrary, it is your digital tool navigating you through the policies and visions of the current Candidates. The goal of the Candymat is to support you to make an informed choice for the upcoming European elections!" />
|
||||
<meta property="og:description" content="The Kandimat is not a regular voting advice application. On the contrary, it is your digital tool navigating you through the policies and visions of the current Candidates. The goal of the Kandimat is to support you to make an informed choice for the upcoming European elections!" />
|
||||
<meta property="og:image" content="https://www.euromat.info/img/facebook.2.png" />
|
||||
<meta property="og:image:secure_url" content="https://www.euromat.info/img/facebook.2.png" />
|
||||
<meta property="fb:app_id" content="766231516835034" />
|
||||
<!-- Add to home screen for Safari on iOS -->
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||
<meta name="apple-mobile-web-app-title" content="Candymat">
|
||||
<meta name="apple-mobile-web-app-title" content="Kandimat">
|
||||
<link rel="apple-touch-icon" href="/img/icons/apple-touch-icon-152x152.png">
|
||||
<!-- Add to home screen for Windows -->
|
||||
<meta name="msapplication-TileImage" content="/img/icons/msapplication-icon-144x144.png">
|
||||
|
@ -51,7 +51,7 @@
|
|||
</script>
|
||||
|
||||
<noscript>
|
||||
<strong>We're sorry but Candymat doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
<strong>We're sorry but Kandimat doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
|
||||
<div id="app"></div>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "CANDYMAT",
|
||||
"short_name": "candymat",
|
||||
"description": "Der Candymat nicht einfach nur ein Wahlomat. Sondern Ihr digitaler Wahl-Freund, der Ihnen einen Eindruck von den politischen Positionen der Kandindaten vermittelt.",
|
||||
"name": "KANDIMAT",
|
||||
"short_name": "kandimat",
|
||||
"description": "Der Kandimat nicht einfach nur ein Wahlomat. Sondern Ihr digitaler Wahl-Freund, der Ihnen einen Eindruck von den politischen Positionen der Kandindaten vermittelt.",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/img/icons/gruenen_logo_200.png",
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
|
||||
data () {
|
||||
return {
|
||||
euromatLogo: require('@/assets/candymat-logo.png'),
|
||||
euromatLogo: require('@/assets/kandimat-logo.png'),
|
||||
logoSize: 220,
|
||||
languages: SUPPORTED_LOCALES.map(([locale, language]) => ({
|
||||
icon: require(`@/assets/svg/flag-${locale}.svg`),
|
||||
|
|
|
@ -37,8 +37,8 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { theses } from '@/data'
|
||||
import { getTranslatedUrl } from '@/i18n/helper'
|
||||
import { apolloThesesQuery, apolloThesesUpdate } from '@/app/euromat/graphqlQueries'
|
||||
|
||||
export default {
|
||||
name: 'Emphasis',
|
||||
|
@ -52,11 +52,18 @@
|
|||
|
||||
data () {
|
||||
return {
|
||||
theses,
|
||||
theses: [],
|
||||
emphasized: []
|
||||
}
|
||||
},
|
||||
|
||||
apollo: {
|
||||
theses: {
|
||||
query: apolloThesesQuery,
|
||||
update: apolloThesesUpdate
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
isEmbedded () {
|
||||
return (
|
||||
|
|
|
@ -11,17 +11,10 @@
|
|||
|
||||
<ul class="party-results">
|
||||
<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-logo">
|
||||
<img
|
||||
v-if="hasPartyLogo(party.token)"
|
||||
:src="getPartyLogo(party.token)"
|
||||
width="50"
|
||||
height="50"
|
||||
:alt="party.token"
|
||||
>
|
||||
<span v-else>{{ party.token }}</span>
|
||||
<span>{{ party.token }}</span>
|
||||
</div>
|
||||
|
||||
<h2>{{ getScorePercentage(party.score) }}%</h2>
|
||||
|
@ -32,31 +25,13 @@
|
|||
<v-progress
|
||||
class="result-percentage"
|
||||
:value="party.score"
|
||||
:max="totalScoredPoints"
|
||||
:max="totalMaxPoints"
|
||||
/>
|
||||
</router-link>
|
||||
|
||||
<div v-if="party.nationalParty" class="party-results-national">
|
||||
<feather-corner-down-right />
|
||||
<div class="party-results-national">
|
||||
<span>
|
||||
{{ $t('results.nationalParty') }}
|
||||
<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>
|
||||
{{ party.name }}
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
|
@ -79,36 +54,16 @@
|
|||
<feather-rotate-cw />
|
||||
</router-link>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { IPDATA_URL } from '@/config/api'
|
||||
import { getTranslatedUrl, getUserLanguage } from '@/i18n/helper'
|
||||
import { getTranslatedUrl } from '@/i18n/helper'
|
||||
import { getPartiesWithScores, getTotalMaxPoints } from '@/app/euromat/scoring'
|
||||
import {
|
||||
MAX_POINTS,
|
||||
BASE_POINTS,
|
||||
MIN_POINTS,
|
||||
EMPHASIS_POINTS,
|
||||
getScoringGrid
|
||||
} from '@/app/euromat/scoring'
|
||||
import { parties } from '@/data'
|
||||
|
||||
const addUp = (a, b) => a + b
|
||||
apolloPersonsForResultsQuery,
|
||||
apolloPersonsForResultsUpdate
|
||||
} from '@/app/euromat/graphqlQueries'
|
||||
|
||||
export default {
|
||||
name: 'Results',
|
||||
|
@ -117,20 +72,15 @@
|
|||
'feather-zoom-in': () =>
|
||||
import('vue-feather-icons/icons/ZoomInIcon' /* webpackChunkName: "icons" */),
|
||||
'feather-rotate-cw': () =>
|
||||
import('vue-feather-icons/icons/RotateCwIcon' /* webpackChunkName: "icons" */),
|
||||
'feather-corner-down-right': () =>
|
||||
import('vue-feather-icons/icons/CornerDownRightIcon' /* webpackChunkName: "icons" */)
|
||||
import('vue-feather-icons/icons/RotateCwIcon' /* webpackChunkName: "icons" */)
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
userCountry: getUserLanguage().country,
|
||||
scoringGrid: [],
|
||||
answers: [],
|
||||
emphasized: [],
|
||||
scores: [],
|
||||
parties,
|
||||
totalScoredPoints: 0
|
||||
parties: [],
|
||||
totalMaxPoints: 0
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -143,14 +93,6 @@
|
|||
this.$route.query.embedded &&
|
||||
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) {
|
||||
this.$router.push({ path: getTranslatedUrl('theses') })
|
||||
await this.$router.push({ path: getTranslatedUrl('theses') })
|
||||
}
|
||||
|
||||
try {
|
||||
const ipResponse = await fetch(IPDATA_URL)
|
||||
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)
|
||||
}
|
||||
const apolloResponse = await this.$apollo.query({ query: apolloPersonsForResultsQuery })
|
||||
const parties = apolloPersonsForResultsUpdate(apolloResponse.data)
|
||||
|
||||
this.emphasized = emphasized
|
||||
this.answers = answers
|
||||
|
||||
this.scoringGrid = getScoringGrid(this.answers, this.emphasized)
|
||||
this.scores = this.getScorePoints(this.scoringGrid)
|
||||
this.parties = this.parties
|
||||
.map(this.getScorePerParty)
|
||||
const partiesWithScores = getPartiesWithScores(answers, emphasized, parties)
|
||||
this.parties = partiesWithScores.map(party => ({
|
||||
id: party.id,
|
||||
token: party.token,
|
||||
score: party.score,
|
||||
name: party.name
|
||||
}))
|
||||
.sort((a, b) => a.score - b.score)
|
||||
.reverse()
|
||||
this.totalScoredPoints = this.scores
|
||||
.map(s => s.highestScore)
|
||||
.reduce(addUp, 0)
|
||||
this.totalMaxPoints = getTotalMaxPoints(answers, emphasized)
|
||||
},
|
||||
|
||||
methods: {
|
||||
getPartyPath (token) {
|
||||
return `${getTranslatedUrl('party')}/${token.toLowerCase()}`
|
||||
},
|
||||
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
|
||||
}
|
||||
}
|
||||
getPartyPath (partyId) {
|
||||
return `${getTranslatedUrl('party')}/${partyId}`
|
||||
},
|
||||
getScorePercentage (score) {
|
||||
return (score / this.totalScoredPoints * 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]
|
||||
}
|
||||
return (score / this.totalMaxPoints * 100).toFixed(2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
<template>
|
||||
<section class="theses">
|
||||
<section v-if="thesesCount > 0 && theses.length > 0" class="theses">
|
||||
<div class="header-progress">
|
||||
<div>
|
||||
<span class="progress-current">{{ currentThesisStep }}</span>
|
||||
<span>/{{ thesesCount }}</span>
|
||||
</div>
|
||||
<button
|
||||
:disabled="currentThesis === 0"
|
||||
:disabled="currentThesisStep === 1"
|
||||
class="btn-dark btn-small"
|
||||
type="button"
|
||||
@click="goBack"
|
||||
|
@ -23,9 +23,9 @@
|
|||
|
||||
<div class="theses-controls">
|
||||
<ul class="theses-btns">
|
||||
<li v-for="option in options" :key="option.label">
|
||||
<button type="button" @click="submitAnswer(option, $event)">
|
||||
{{ option.label }} <component :is="'feather-' + option.icon" />
|
||||
<li v-for="possiblePosition in possiblePositions" :key="possiblePosition.label">
|
||||
<button type="button" @click="submitAnswer(possiblePosition, $event)">
|
||||
{{ possiblePosition.label }} <component :is="'feather-' + possiblePosition.icon" />
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -35,17 +35,27 @@
|
|||
type="button"
|
||||
@click="submitAnswer(optionSkip)"
|
||||
>
|
||||
{{ optionSkip.label }} <feather-corner-up-right />
|
||||
{{ optionSkip.label }}
|
||||
<feather-corner-up-right />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section v-else>
|
||||
<span>Loading...</span>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { options, theses } from '@/data'
|
||||
import possiblePositions from '@/app/euromat/possiblePositions'
|
||||
import { getTranslatedUrl } from '@/i18n/helper'
|
||||
import {
|
||||
apolloThesesCountQuery,
|
||||
apolloThesesCountUpdate,
|
||||
apolloThesesQuery,
|
||||
apolloThesesUpdate
|
||||
} from '@/app/euromat/graphqlQueries'
|
||||
|
||||
export default {
|
||||
name: 'EuroMat',
|
||||
|
@ -63,12 +73,24 @@
|
|||
|
||||
data () {
|
||||
return {
|
||||
currentThesis: 0,
|
||||
thesesCount: theses.length,
|
||||
currentThesisStep: 1,
|
||||
theses: [],
|
||||
thesesCount: 0,
|
||||
answers: []
|
||||
}
|
||||
},
|
||||
|
||||
apollo: {
|
||||
theses: {
|
||||
query: apolloThesesQuery,
|
||||
update: apolloThesesUpdate
|
||||
},
|
||||
thesesCount: {
|
||||
query: apolloThesesCountQuery,
|
||||
update: apolloThesesCountUpdate
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
isEmbedded () {
|
||||
return (
|
||||
|
@ -76,23 +98,20 @@
|
|||
this.$route.query.embedded === 'iframe'
|
||||
)
|
||||
},
|
||||
currentThesisStep () {
|
||||
return this.currentThesis + 1
|
||||
},
|
||||
thesisTitle () {
|
||||
if (this.currentThesis === this.thesesCount) {
|
||||
if (this.currentThesisStep > this.thesesCount) {
|
||||
return
|
||||
}
|
||||
return this.getThesis(this.currentThesis).thesis[this.$i18n.locale]
|
||||
return this.theses[this.currentThesisStep - 1].thesis[this.$i18n.locale]
|
||||
},
|
||||
thesisCategory () {
|
||||
if (this.currentThesis === this.thesesCount) {
|
||||
if (this.currentThesisStep > this.thesesCount) {
|
||||
return
|
||||
}
|
||||
return this.getThesis(this.currentThesis).category[this.$i18n.locale]
|
||||
return this.theses[this.currentThesisStep - 1].category[this.$i18n.locale]
|
||||
},
|
||||
options () {
|
||||
return options.map(option =>
|
||||
possiblePositions () {
|
||||
return possiblePositions.map(option =>
|
||||
Object.assign({}, option, {
|
||||
label: this.$t(`theses.${option.position}`),
|
||||
icon: this.getIconName(option.position)
|
||||
|
@ -100,7 +119,7 @@
|
|||
.filter(option => option.position !== 'skipped')
|
||||
},
|
||||
optionSkip () {
|
||||
const skipped = options.find(option => option.position === 'skipped')
|
||||
const skipped = possiblePositions.find(option => option.position === 'skipped')
|
||||
return Object.assign({}, skipped, {
|
||||
label: this.$t('theses.skipped')
|
||||
})
|
||||
|
@ -117,14 +136,11 @@
|
|||
default:
|
||||
}
|
||||
},
|
||||
getThesis (id) {
|
||||
return theses.find(t => t.id === id)
|
||||
},
|
||||
goBack () {
|
||||
const thesis = this.getThesis(this.currentThesis)
|
||||
const thesis = this.theses[this.currentThesisStep - 1]
|
||||
const index = this.answers.findIndex(a => a.thesis === thesis.id)
|
||||
this.answers.splice(index, 1)
|
||||
this.currentThesis -= 1
|
||||
this.currentThesisStep -= 1
|
||||
},
|
||||
submitAnswer (option, event) {
|
||||
if (!option) {
|
||||
|
@ -132,13 +148,13 @@
|
|||
return console.warn('Invalid answer')
|
||||
}
|
||||
|
||||
const thesis = this.getThesis(this.currentThesis)
|
||||
const thesis = this.theses[this.currentThesisStep - 1]
|
||||
this.answers.push({ thesis: thesis.id, position: option.position })
|
||||
this.currentThesis += 1
|
||||
this.currentThesisStep += 1
|
||||
event && event.target.blur()
|
||||
window.scrollTo(0, 0)
|
||||
|
||||
if (this.currentThesis === this.thesesCount) {
|
||||
if (this.currentThesisStep > this.thesesCount) {
|
||||
this.forwardToResults()
|
||||
}
|
||||
},
|
||||
|
|
109
src/app/euromat/graphqlQueries.js
Normal file
109
src/app/euromat/graphqlQueries.js
Normal file
|
@ -0,0 +1,109 @@
|
|||
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: ROW_ID_ASC) {
|
||||
nodes {
|
||||
category: categoryByCategoryRowId {
|
||||
id
|
||||
title
|
||||
}
|
||||
title
|
||||
rowId
|
||||
id
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
export const apolloThesesUpdate = data => data.allQuestions.nodes.map(node => ({
|
||||
id: node.rowId,
|
||||
thesis: {
|
||||
de: node.title
|
||||
},
|
||||
category: {
|
||||
de: node.category ? node.category.title : ''
|
||||
}
|
||||
}))
|
||||
|
||||
export const apolloThesesCountQuery = gql`{
|
||||
allQuestions {
|
||||
totalCount
|
||||
}
|
||||
}`
|
||||
|
||||
export const apolloThesesCountUpdate = data => data.allQuestions.totalCount
|
||||
|
||||
export const apolloPersonsForResultsQuery = gql`{
|
||||
allPeople(condition: {role: KANDIMAT_CANDIDATE}) {
|
||||
nodes {
|
||||
id
|
||||
firstName
|
||||
lastName
|
||||
rowId
|
||||
answers: answersByPersonRowId {
|
||||
nodes {
|
||||
id
|
||||
position
|
||||
questionRowId
|
||||
text
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
export const apolloPersonsForResultsUpdate = data => data.allPeople.nodes.map(person => ({
|
||||
id: person.rowId,
|
||||
name: `${person.firstName} ${person.lastName}`,
|
||||
token: person.firstName.charAt(0) + person.lastName.charAt(0),
|
||||
positions: person.answers.nodes.map(answer => ({
|
||||
thesis: answer.questionRowId,
|
||||
position: getPositionById(answer.position),
|
||||
statement: {
|
||||
de: answer.text
|
||||
}
|
||||
}))
|
||||
}))
|
||||
|
||||
export const apolloPersonPositionsQuery = gql`
|
||||
query Person($partyId: Int!) {
|
||||
personByRowId(rowId: $partyId) {
|
||||
id
|
||||
firstName
|
||||
lastName
|
||||
answers: answersByPersonRowId {
|
||||
nodes {
|
||||
id
|
||||
position
|
||||
personRowId
|
||||
text
|
||||
question: questionByQuestionRowId {
|
||||
id
|
||||
rowId
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
export const apolloPersonPositionsUpdate = data => {
|
||||
const person = data.personByRowId
|
||||
return {
|
||||
id: person.rowId,
|
||||
name: `${person.firstName} ${person.lastName}`,
|
||||
token: person.firstName.charAt(0) + person.lastName.charAt(0),
|
||||
theses: person.answers ? person.answers.nodes.map(answer => {
|
||||
const question = answer.question
|
||||
return question ? {
|
||||
id: question.rowId,
|
||||
position: getPositionById(answer.position),
|
||||
statement: answer.text,
|
||||
showStatement: false
|
||||
} : null
|
||||
}) : []
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
const options = [
|
||||
const possiblePositions = [
|
||||
{
|
||||
'position': 'positive',
|
||||
'id': 0
|
||||
|
@ -16,5 +16,4 @@ const options = [
|
|||
'id': 3
|
||||
}
|
||||
]
|
||||
|
||||
export default options
|
||||
export default possiblePositions
|
|
@ -1,21 +1,60 @@
|
|||
import { parties } from '@/data'
|
||||
|
||||
export const MAX_POINTS = 2
|
||||
export const BASE_POINTS = 1
|
||||
export const MIN_POINTS = 0
|
||||
export const EMPHASIS_POINTS = 2
|
||||
|
||||
export function getPartyPositions (thesis) {
|
||||
return parties.map(party => {
|
||||
const position = party.positions.find(p => p.thesis === thesis)
|
||||
export function getPartiesWithScores (answers, emphasized, partiesPositions) {
|
||||
const scorePointsGrid = getScorePointsGrid(answers, emphasized, partiesPositions)
|
||||
|
||||
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 {
|
||||
type: 'party',
|
||||
party: party.id,
|
||||
position: (position && position.position) || {}
|
||||
thesis: row.thesis,
|
||||
scores
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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:
|
||||
// [
|
||||
// {
|
||||
|
@ -33,15 +72,50 @@ export function getPartyPositions (thesis) {
|
|||
// },
|
||||
// ...
|
||||
// ]
|
||||
export function getScoringGrid (userAnswers, emphasizedTheses) {
|
||||
return userAnswers.map(answer => (
|
||||
function getScoringGrid (userAnswers, emphasizedTheses, parties) {
|
||||
const grid = userAnswers.map(answer => (
|
||||
{
|
||||
thesis: answer.thesis,
|
||||
emphasis: emphasizedTheses.filter(e => e.thesis === answer.thesis).length >= 1,
|
||||
positions: [
|
||||
...getPartyPositions(answer.thesis),
|
||||
...getPartyPositions(answer.thesis, parties),
|
||||
...[{ type: 'user', position: answer.position }]
|
||||
]
|
||||
}
|
||||
))
|
||||
return grid
|
||||
}
|
||||
|
||||
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) || 'skipped'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function evalPointsPerThesisPerParty (partyPosition, userPosition, emphasis) {
|
||||
let score = 0
|
||||
if (partyPosition === 'skipped' || userPosition === 'skipped') {
|
||||
score = MIN_POINTS
|
||||
} else 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
|
||||
}
|
||||
|
|
115
src/app/euromat/scoring.test.js
Normal file
115
src/app/euromat/scoring.test.js
Normal file
|
@ -0,0 +1,115 @@
|
|||
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}
|
||||
${'negative'} | ${'skipped'} | ${0} | ${0}
|
||||
${'neutral'} | ${'negative'} | ${1} | ${2}
|
||||
${'neutral'} | ${'neutral'} | ${2} | ${4}
|
||||
${'neutral'} | ${'positive'} | ${1} | ${2}
|
||||
${'neutral'} | ${'skipped'} | ${0} | ${0}
|
||||
${'positive'} | ${'negative'} | ${0} | ${0}
|
||||
${'positive'} | ${'neutral'} | ${1} | ${2}
|
||||
${'positive'} | ${'positive'} | ${2} | ${4}
|
||||
${'positive'} | ${'skipped'} | ${0} | ${0}
|
||||
${'skipped'} | ${'negative'} | ${0} | ${0}
|
||||
${'skipped'} | ${'neutral'} | ${0} | ${0}
|
||||
${'skipped'} | ${'positive'} | ${0} | ${0}
|
||||
${'skipped'} | ${'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>
|
||||
<section>
|
||||
<header :class="['party-header', { 'no-party-link': !partyProgramLink }]">
|
||||
<div :class="['party-header-logo', { 'no-logo': !hasPartyLogo }]">
|
||||
<img
|
||||
v-if="hasPartyLogo"
|
||||
:src="partyLogo"
|
||||
:width="logoSize"
|
||||
:height="logoSize"
|
||||
:alt="partyName"
|
||||
>
|
||||
<span v-else>
|
||||
{{ partyToken }}
|
||||
<div :class="['party-header-logo', 'no-logo']">
|
||||
<span>
|
||||
{{ party.token }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="party-header-info">
|
||||
<router-link v-if="!!answers"
|
||||
class="btn btn-dark btn-small"
|
||||
:to="{ path: resultsPath }"
|
||||
class="btn btn-dark btn-small"
|
||||
:to="{ path: resultsPath }"
|
||||
>
|
||||
{{ $t('party.backButtonLabel') }}
|
||||
<feather-corner-up-left />
|
||||
</router-link>
|
||||
<h1>{{ partyName }}</h1>
|
||||
<h1>{{ party.name }}</h1>
|
||||
<a
|
||||
v-if="!!partyProgramLink"
|
||||
class="btn"
|
||||
|
@ -38,9 +31,9 @@
|
|||
<div class="theses-legend">
|
||||
<p>{{ $t('party.legendLabel') }}:</p>
|
||||
<ul>
|
||||
<li v-for="option in options" :key="option.position">
|
||||
<component :is="'feather-' + positionToIconName(option.position)" />
|
||||
<span>{{ $t(`theses.${option.position}`) }}</span>
|
||||
<li v-for="possiblePosition in possiblePositions" :key="possiblePosition.position">
|
||||
<component :is="'feather-' + positionToIconName(possiblePosition.position)" />
|
||||
<span>{{ $t(`theses.${possiblePosition.position}`) }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
@ -56,34 +49,34 @@
|
|||
|
||||
<li v-for="thesis in theses" :key="thesis.id">
|
||||
<div class="thesis-facts">
|
||||
<div class="list-thesis" @click="toggleStatement(thesis.id)">
|
||||
<div class="list-thesis" @click="toggleStatement(thesis)">
|
||||
<div class="thesis-subline">
|
||||
<component :is="'feather-' + chevronIcon(thesis.id)" />
|
||||
<span>{{ getCategory(thesis.category) }}</span>
|
||||
<component :is="'feather-' + chevronIcon(thesis.showStatement)" />
|
||||
<span>{{ thesis.category[$i18n.locale] }}</span>
|
||||
</div>
|
||||
<h3>{{ getThesis(thesis.thesis) }}</h3>
|
||||
<h3>{{ thesis.thesis[$i18n.locale] }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="statements-party">
|
||||
<component :is="'feather-' + getPartyPosition(thesis.id)" />
|
||||
<component :is="'feather-' + positionToIconName(getPartyPosition(thesis))" />
|
||||
</div>
|
||||
<div v-if="!!answers" class="statements-user">
|
||||
<component :is="'feather-' + getUserPosition(thesis.id)" />
|
||||
</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>
|
||||
<blockquote>
|
||||
{{ getPartyStatement(thesis.id) }}
|
||||
{{ getPartyStatement(thesis) }}
|
||||
</blockquote>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<router-link v-if="!!answers"
|
||||
class="btn btn-dark btn-small"
|
||||
:to="{ path: resultsPath }"
|
||||
class="btn btn-dark btn-small"
|
||||
:to="{ path: resultsPath }"
|
||||
>
|
||||
{{ $t('party.backButtonLabel') }}
|
||||
<feather-corner-up-left />
|
||||
|
@ -92,8 +85,14 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { parties, theses, options } from '@/data'
|
||||
import possiblePositions from '@/app/euromat/possiblePositions'
|
||||
import { getTranslatedUrl } from '@/i18n/helper'
|
||||
import {
|
||||
apolloPersonPositionsQuery,
|
||||
apolloPersonPositionsUpdate,
|
||||
apolloThesesQuery, apolloThesesUpdate
|
||||
} from '@/app/euromat/graphqlQueries'
|
||||
|
||||
export default {
|
||||
name: 'Party',
|
||||
|
||||
|
@ -127,48 +126,63 @@
|
|||
|
||||
return {
|
||||
logoSize: 60,
|
||||
partyLogo: this.hasPartyLogo && require(`@/assets/svg/${this.$route.params.token}-logo.svg`),
|
||||
partyToken: this.$route.params.token.toUpperCase(),
|
||||
party: parties.find(p => p.token === this.$route.params.token.toUpperCase()),
|
||||
partyId: this.$route.params.token,
|
||||
party: {
|
||||
id: this.$route.params.token,
|
||||
token: this.$route.params.token,
|
||||
name: 'Loading ...',
|
||||
theses: []
|
||||
},
|
||||
theses: [],
|
||||
answers,
|
||||
toggles: theses.map(t => ({ id: t.id, show: false })),
|
||||
theses,
|
||||
options
|
||||
possiblePositions
|
||||
}
|
||||
},
|
||||
|
||||
apollo: {
|
||||
party: {
|
||||
query: apolloPersonPositionsQuery,
|
||||
variables () {
|
||||
return {
|
||||
partyId: parseInt(this.$route.params.token)
|
||||
}
|
||||
},
|
||||
update: apolloPersonPositionsUpdate
|
||||
},
|
||||
theses: {
|
||||
query: apolloThesesQuery,
|
||||
update: data => {
|
||||
const allTheses = apolloThesesUpdate(data)
|
||||
return allTheses.map(thesis => ({ ...thesis, showStatement: false }))
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
resultsPath () {
|
||||
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 () {
|
||||
return this.party.program[this.$i18n.locale]
|
||||
return false
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
chevronIcon (id) {
|
||||
return this.showStatement(id)
|
||||
getPartyAnswerForThesis (thesis) {
|
||||
return this.party.theses.find(partyThesis => partyThesis.id === thesis.id)
|
||||
},
|
||||
getPartyPosition (thesis) {
|
||||
const partyAnswerForThesis = this.getPartyAnswerForThesis(thesis)
|
||||
return partyAnswerForThesis ? partyAnswerForThesis.position : 'skipped'
|
||||
},
|
||||
getPartyStatement (thesis) {
|
||||
const partyAnswerForThesis = this.getPartyAnswerForThesis(thesis)
|
||||
return partyAnswerForThesis ? partyAnswerForThesis.statement : ''
|
||||
},
|
||||
chevronIcon (showStatement) {
|
||||
return showStatement
|
||||
? 'chevron-up'
|
||||
: 'chevron-down'
|
||||
},
|
||||
getCategory (category) {
|
||||
return category[this.$i18n.locale]
|
||||
},
|
||||
getThesis (thesis) {
|
||||
return thesis[this.$i18n.locale]
|
||||
},
|
||||
positionToIconName (position) {
|
||||
switch (position) {
|
||||
case 'positive':
|
||||
|
@ -182,27 +196,13 @@
|
|||
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) {
|
||||
return this.positionToIconName(
|
||||
this.answers.find(a => a.thesis === id).position
|
||||
)
|
||||
},
|
||||
showStatement (id) {
|
||||
return this.toggles.find(t => t.id === id).show
|
||||
},
|
||||
toggleStatement (id) {
|
||||
const statement = this.toggles.find(t => t.id === id)
|
||||
statement.show = !statement.show
|
||||
toggleStatement (thesis) {
|
||||
thesis.showStatement = !thesis.showStatement
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
|
@ -1 +0,0 @@
|
|||
export const IPDATA_URL = 'https://api.ipdata.co/?api-key=test'
|
|
@ -1,7 +1,4 @@
|
|||
import { loadContent } from '@/helper/content'
|
||||
import options from './options'
|
||||
import theses from './theses'
|
||||
import parties from './parties'
|
||||
|
||||
const i18n = loadContent(
|
||||
'meta',
|
||||
|
@ -9,8 +6,5 @@ const i18n = loadContent(
|
|||
)
|
||||
|
||||
export {
|
||||
options,
|
||||
theses,
|
||||
parties,
|
||||
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 '@/registerServiceWorker'
|
||||
import VueApollo from 'vue-apollo'
|
||||
import ApolloClient from 'apollo-boost'
|
||||
|
||||
Vue.config.productionTip = false
|
||||
Vue.use(VueSVGIcon)
|
||||
Vue.use(storage)
|
||||
Vue.use(VueApollo)
|
||||
|
||||
const apolloClient = new ApolloClient({
|
||||
uri: 'http://localhost:5433/graphql'
|
||||
})
|
||||
|
||||
const apolloProvider = new VueApollo({
|
||||
defaultClient: apolloClient
|
||||
})
|
||||
|
||||
new Vue({
|
||||
i18n,
|
||||
router,
|
||||
apolloProvider,
|
||||
data: {
|
||||
backupStorage: {
|
||||
answers: undefined,
|
||||
|
|
Loading…
Reference in a new issue