Last not Least: Get party details page from backend
Also * reorder graphQl queries: Always a query constant + update function * use id instead of token (initials) to identify person in url * Remove any party logo related sections since currently not needed * Move "options" from data closer to actual code since it doesn't really belong to data (data may be deleted in the future, anyway) * add nodeId in gql queries wherever possible to avoid problems with the apollo cache * add note about graphql schema import to README.md
This commit is contained in:
parent
5512955af4
commit
64e6b861b6
|
@ -25,4 +25,4 @@ yarn-error.log*
|
|||
*.sw*
|
||||
|
||||
# GraphQl plugin related
|
||||
./postgraphile-schema.graphql
|
||||
postgraphile-schema.graphql
|
||||
|
|
11
README.md
11
README.md
|
@ -16,10 +16,19 @@ 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.
|
||||
|
|
|
@ -38,7 +38,7 @@
|
|||
|
||||
<script>
|
||||
import { getTranslatedUrl } from '@/i18n/helper'
|
||||
import { apolloTheses } from '@/app/euromat/graphqlQueries'
|
||||
import { apolloThesesQuery, apolloThesesUpdate } from '@/app/euromat/graphqlQueries'
|
||||
|
||||
export default {
|
||||
name: 'Emphasis',
|
||||
|
@ -58,7 +58,10 @@
|
|||
},
|
||||
|
||||
apollo: {
|
||||
theses: apolloTheses
|
||||
theses: {
|
||||
query: apolloThesesQuery,
|
||||
update: apolloThesesUpdate
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
|
|
@ -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>
|
||||
|
@ -67,7 +60,10 @@
|
|||
<script>
|
||||
import { getTranslatedUrl } from '@/i18n/helper'
|
||||
import { getPartiesWithScores, getTotalMaxPoints } from '@/app/euromat/scoring'
|
||||
import { apolloPersonsForResults, parseApolloPersonForResults } from '@/app/euromat/graphqlQueries'
|
||||
import {
|
||||
apolloPersonsForResultsQuery,
|
||||
apolloPersonsForResultsUpdate
|
||||
} from '@/app/euromat/graphqlQueries'
|
||||
|
||||
export default {
|
||||
name: 'Results',
|
||||
|
@ -116,11 +112,12 @@
|
|||
await this.$router.push({ path: getTranslatedUrl('theses') })
|
||||
}
|
||||
|
||||
const apolloResponse = await this.$apollo.query(apolloPersonsForResults)
|
||||
const parties = parseApolloPersonForResults(apolloResponse.data)
|
||||
const apolloResponse = await this.$apollo.query({ query: apolloPersonsForResultsQuery })
|
||||
const parties = apolloPersonsForResultsUpdate(apolloResponse.data)
|
||||
|
||||
const partiesWithScores = getPartiesWithScores(answers, emphasized, parties)
|
||||
this.parties = partiesWithScores.map(party => ({
|
||||
id: party.id,
|
||||
token: party.token,
|
||||
score: party.score,
|
||||
name: party.name
|
||||
|
@ -131,33 +128,8 @@
|
|||
},
|
||||
|
||||
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.totalMaxPoints * 100).toFixed(2)
|
||||
|
|
|
@ -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>
|
||||
|
@ -48,9 +48,14 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { options } from '@/data'
|
||||
import possiblePositions from '@/app/euromat/possiblePositions'
|
||||
import { getTranslatedUrl } from '@/i18n/helper'
|
||||
import { apolloTheses, apolloThesesCount } from '@/app/euromat/graphqlQueries'
|
||||
import {
|
||||
apolloThesesCountQuery,
|
||||
apolloThesesCountUpdate,
|
||||
apolloThesesQuery,
|
||||
apolloThesesUpdate
|
||||
} from '@/app/euromat/graphqlQueries'
|
||||
|
||||
export default {
|
||||
name: 'EuroMat',
|
||||
|
@ -76,8 +81,14 @@
|
|||
},
|
||||
|
||||
apollo: {
|
||||
theses: apolloTheses,
|
||||
thesesCount: apolloThesesCount
|
||||
theses: {
|
||||
query: apolloThesesQuery,
|
||||
update: apolloThesesUpdate
|
||||
},
|
||||
thesesCount: {
|
||||
query: apolloThesesCountQuery,
|
||||
update: apolloThesesCountUpdate
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
@ -102,8 +113,8 @@
|
|||
}
|
||||
return this.getThesis(this.currentThesis).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)
|
||||
|
@ -111,7 +122,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')
|
||||
})
|
||||
|
|
|
@ -1,19 +1,25 @@
|
|||
import gql from 'graphql-tag'
|
||||
import { options } from '@/data'
|
||||
import possiblePositions from '@/app/euromat/possiblePositions'
|
||||
|
||||
export const apolloTheses = {
|
||||
query: gql`{
|
||||
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
|
||||
}
|
||||
}
|
||||
}`,
|
||||
update: data => data.allQuestions.nodes.map(node => ({
|
||||
}`
|
||||
|
||||
export const apolloThesesUpdate = data => data.allQuestions.nodes.map(node => ({
|
||||
id: node.id,
|
||||
thesis: {
|
||||
de: node.text
|
||||
|
@ -22,30 +28,25 @@ export const apolloTheses = {
|
|||
de: node.category.title
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
export const apolloThesesCount = {
|
||||
query: gql`{
|
||||
export const apolloThesesCountQuery = gql`{
|
||||
allQuestions {
|
||||
totalCount
|
||||
}
|
||||
}`,
|
||||
update: data => data.allQuestions.totalCount
|
||||
}
|
||||
}`
|
||||
|
||||
function getPositionById (id) {
|
||||
return options.find(option => option.id === id).position
|
||||
}
|
||||
export const apolloThesesCountUpdate = data => data.allQuestions.totalCount
|
||||
|
||||
export const apolloPersonsForResults = {
|
||||
query: gql`{
|
||||
export const apolloPersonsForResultsQuery = gql`{
|
||||
allPeople(condition: {role: CANDYMAT_CANDIDATE}) {
|
||||
nodes {
|
||||
nodeId
|
||||
firstName
|
||||
lastName
|
||||
id
|
||||
answersByPersonId {
|
||||
nodes {
|
||||
nodeId
|
||||
position
|
||||
questionId
|
||||
text
|
||||
|
@ -54,10 +55,8 @@ export const apolloPersonsForResults = {
|
|||
}
|
||||
}
|
||||
}`
|
||||
}
|
||||
|
||||
export function parseApolloPersonForResults (data) {
|
||||
return data.allPeople.nodes.map(person => ({
|
||||
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),
|
||||
|
@ -69,4 +68,48 @@ export function parseApolloPersonForResults (data) {
|
|||
}
|
||||
}))
|
||||
}))
|
||||
|
||||
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',
|
||||
'id': 0
|
||||
|
@ -16,5 +16,4 @@ const options = [
|
|||
'id': 3
|
||||
}
|
||||
]
|
||||
|
||||
export default options
|
||||
export default possiblePositions
|
|
@ -1,16 +1,9 @@
|
|||
<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>
|
||||
|
||||
|
@ -22,7 +15,7 @@
|
|||
{{ $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>
|
||||
|
@ -54,28 +47,28 @@
|
|||
</h2>
|
||||
</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="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 }}</span>
|
||||
</div>
|
||||
<h3>{{ getThesis(thesis.thesis) }}</h3>
|
||||
<h3>{{ thesis.thesis }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="statements-party">
|
||||
<component :is="'feather-' + getPartyPosition(thesis.id)" />
|
||||
<component :is="'feather-' + positionToIconName(thesis.position)" />
|
||||
</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) }}
|
||||
{{ thesis.statement }}
|
||||
</blockquote>
|
||||
</div>
|
||||
</li>
|
||||
|
@ -92,8 +85,10 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { parties, theses, options } from '@/data'
|
||||
import possiblePositions from '@/app/euromat/possiblePositions'
|
||||
import { getTranslatedUrl } from '@/i18n/helper'
|
||||
import { apolloPersonPositionsQuery, apolloPersonPositionsUpdate } from '@/app/euromat/graphqlQueries'
|
||||
|
||||
export default {
|
||||
name: 'Party',
|
||||
|
||||
|
@ -127,13 +122,27 @@
|
|||
|
||||
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: []
|
||||
},
|
||||
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
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -141,34 +150,17 @@
|
|||
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)
|
||||
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 +174,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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
Loading…
Reference in New Issue