mirror of
https://github.com/IT4Change/Ocelot-Social.git
synced 2025-12-13 07:45:56 +00:00
Merge pull request #2194 from Human-Connection/donation-info
Add donation status and button
This commit is contained in:
commit
25adced793
@ -135,6 +135,7 @@ const permissions = shield(
|
||||
blockedUsers: isAuthenticated,
|
||||
notifications: isAuthenticated,
|
||||
profilePagePosts: or(onlyEnabledContent, isModerator),
|
||||
Donations: isAuthenticated,
|
||||
},
|
||||
Mutation: {
|
||||
'*': deny,
|
||||
@ -177,6 +178,7 @@ const permissions = shield(
|
||||
VerifyEmailAddress: isAuthenticated,
|
||||
pinPost: isAdmin,
|
||||
unpinPost: isAdmin,
|
||||
UpdateDonations: isAdmin,
|
||||
},
|
||||
User: {
|
||||
email: or(isMyOwn, isAdmin),
|
||||
|
||||
14
backend/src/models/Donations.js
Normal file
14
backend/src/models/Donations.js
Normal file
@ -0,0 +1,14 @@
|
||||
import uuid from 'uuid/v4'
|
||||
|
||||
module.exports = {
|
||||
id: { type: 'string', primary: true, default: uuid },
|
||||
goal: { type: 'number' },
|
||||
progress: { type: 'number' },
|
||||
createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
|
||||
updatedAt: {
|
||||
type: 'string',
|
||||
isoDate: true,
|
||||
required: true,
|
||||
default: () => new Date().toISOString(),
|
||||
},
|
||||
}
|
||||
@ -12,4 +12,5 @@ export default {
|
||||
Category: require('./Category.js'),
|
||||
Tag: require('./Tag.js'),
|
||||
Location: require('./Location.js'),
|
||||
Donations: require('./Donations.js'),
|
||||
}
|
||||
|
||||
@ -24,6 +24,7 @@ export default applyScalars(
|
||||
'SocialMedia',
|
||||
'NOTIFIED',
|
||||
'REPORTED',
|
||||
'Donations',
|
||||
],
|
||||
// add 'User' here as soon as possible
|
||||
},
|
||||
@ -44,6 +45,7 @@ export default applyScalars(
|
||||
'EMOTED',
|
||||
'NOTIFIED',
|
||||
'REPORTED',
|
||||
'Donations',
|
||||
],
|
||||
// add 'User' here as soon as possible
|
||||
},
|
||||
|
||||
32
backend/src/schema/resolvers/donations.js
Normal file
32
backend/src/schema/resolvers/donations.js
Normal file
@ -0,0 +1,32 @@
|
||||
export default {
|
||||
Mutation: {
|
||||
UpdateDonations: async (_parent, params, context, _resolveInfo) => {
|
||||
const { driver } = context
|
||||
const session = driver.session()
|
||||
let donations
|
||||
const writeTxResultPromise = session.writeTransaction(async txc => {
|
||||
const updateDonationsTransactionResponse = await txc.run(
|
||||
`
|
||||
MATCH (donations:Donations)
|
||||
WITH donations LIMIT 1
|
||||
SET donations += $params
|
||||
SET donations.updatedAt = toString(datetime())
|
||||
RETURN donations
|
||||
`,
|
||||
{ params },
|
||||
)
|
||||
return updateDonationsTransactionResponse.records.map(
|
||||
record => record.get('donations').properties,
|
||||
)
|
||||
})
|
||||
try {
|
||||
const txResult = await writeTxResultPromise
|
||||
if (!txResult[0]) return null
|
||||
donations = txResult[0]
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
return donations
|
||||
},
|
||||
},
|
||||
}
|
||||
174
backend/src/schema/resolvers/donations.spec.js
Normal file
174
backend/src/schema/resolvers/donations.spec.js
Normal file
@ -0,0 +1,174 @@
|
||||
import { createTestClient } from 'apollo-server-testing'
|
||||
import Factory from '../../seed/factories'
|
||||
import { gql } from '../../jest/helpers'
|
||||
import { neode as getNeode, getDriver } from '../../bootstrap/neo4j'
|
||||
import createServer from '../../server'
|
||||
|
||||
let mutate, query, authenticatedUser, variables
|
||||
const factory = Factory()
|
||||
const instance = getNeode()
|
||||
const driver = getDriver()
|
||||
|
||||
const updateDonationsMutation = gql`
|
||||
mutation($goal: Int, $progress: Int) {
|
||||
UpdateDonations(goal: $goal, progress: $progress) {
|
||||
id
|
||||
goal
|
||||
progress
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
`
|
||||
const donationsQuery = gql`
|
||||
query {
|
||||
Donations {
|
||||
id
|
||||
goal
|
||||
progress
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
describe('donations', () => {
|
||||
let currentUser, newlyCreatedDonations
|
||||
beforeAll(async () => {
|
||||
await factory.cleanDatabase()
|
||||
authenticatedUser = undefined
|
||||
const { server } = createServer({
|
||||
context: () => {
|
||||
return {
|
||||
driver,
|
||||
neode: instance,
|
||||
user: authenticatedUser,
|
||||
}
|
||||
},
|
||||
})
|
||||
mutate = createTestClient(server).mutate
|
||||
query = createTestClient(server).query
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
variables = {}
|
||||
newlyCreatedDonations = await factory.create('Donations')
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await factory.cleanDatabase()
|
||||
})
|
||||
|
||||
describe('query for donations', () => {
|
||||
describe('unauthenticated', () => {
|
||||
it('throws authorization error', async () => {
|
||||
authenticatedUser = undefined
|
||||
await expect(query({ query: donationsQuery, variables })).resolves.toMatchObject({
|
||||
errors: [{ message: 'Not Authorised!' }],
|
||||
})
|
||||
})
|
||||
|
||||
describe('authenticated', () => {
|
||||
beforeEach(async () => {
|
||||
currentUser = await factory.create('User', {
|
||||
id: 'normal-user',
|
||||
role: 'user',
|
||||
})
|
||||
authenticatedUser = await currentUser.toJson()
|
||||
})
|
||||
|
||||
it('returns the current Donations info', async () => {
|
||||
await expect(query({ query: donationsQuery, variables })).resolves.toMatchObject({
|
||||
data: { Donations: [{ goal: 15000, progress: 0 }] },
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('update donations', () => {
|
||||
beforeEach(() => {
|
||||
variables = { goal: 20000, progress: 3000 }
|
||||
})
|
||||
|
||||
describe('unauthenticated', () => {
|
||||
it('throws authorization error', async () => {
|
||||
authenticatedUser = undefined
|
||||
await expect(
|
||||
mutate({ mutation: updateDonationsMutation, variables }),
|
||||
).resolves.toMatchObject({
|
||||
errors: [{ message: 'Not Authorised!' }],
|
||||
})
|
||||
})
|
||||
|
||||
describe('authenticated', () => {
|
||||
describe('as a normal user', () => {
|
||||
beforeEach(async () => {
|
||||
currentUser = await factory.create('User', {
|
||||
id: 'normal-user',
|
||||
role: 'user',
|
||||
})
|
||||
authenticatedUser = await currentUser.toJson()
|
||||
})
|
||||
|
||||
it('throws authorization error', async () => {
|
||||
await expect(
|
||||
mutate({ mutation: updateDonationsMutation, variables }),
|
||||
).resolves.toMatchObject({
|
||||
data: { UpdateDonations: null },
|
||||
errors: [{ message: 'Not Authorised!' }],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('as a moderator', () => {
|
||||
beforeEach(async () => {
|
||||
currentUser = await factory.create('User', {
|
||||
id: 'moderator',
|
||||
role: 'moderator',
|
||||
})
|
||||
authenticatedUser = await currentUser.toJson()
|
||||
})
|
||||
|
||||
it('throws authorization error', async () => {
|
||||
await expect(
|
||||
mutate({ mutation: updateDonationsMutation, variables }),
|
||||
).resolves.toMatchObject({
|
||||
data: { UpdateDonations: null },
|
||||
errors: [{ message: 'Not Authorised!' }],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('as an admin', () => {
|
||||
beforeEach(async () => {
|
||||
currentUser = await factory.create('User', {
|
||||
id: 'admin',
|
||||
role: 'admin',
|
||||
})
|
||||
authenticatedUser = await currentUser.toJson()
|
||||
})
|
||||
|
||||
it('updates Donations info', async () => {
|
||||
await expect(
|
||||
mutate({ mutation: updateDonationsMutation, variables }),
|
||||
).resolves.toMatchObject({
|
||||
data: { UpdateDonations: { goal: 20000, progress: 3000 } },
|
||||
errors: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
it('updates the updatedAt attribute', async () => {
|
||||
newlyCreatedDonations = await newlyCreatedDonations.toJson()
|
||||
const {
|
||||
data: { UpdateDonations },
|
||||
} = await mutate({ mutation: updateDonationsMutation, variables })
|
||||
expect(newlyCreatedDonations.updatedAt).toBeTruthy()
|
||||
expect(Date.parse(newlyCreatedDonations.updatedAt)).toEqual(expect.any(Number))
|
||||
expect(UpdateDonations.updatedAt).toBeTruthy()
|
||||
expect(Date.parse(UpdateDonations.updatedAt)).toEqual(expect.any(Number))
|
||||
expect(newlyCreatedDonations.updatedAt).not.toEqual(UpdateDonations.updatedAt)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
15
backend/src/schema/types/type/Donations.gql
Normal file
15
backend/src/schema/types/type/Donations.gql
Normal file
@ -0,0 +1,15 @@
|
||||
type Donations {
|
||||
id: ID!
|
||||
goal: Int!
|
||||
progress: Int!
|
||||
createdAt: String
|
||||
updatedAt: String
|
||||
}
|
||||
|
||||
type Query {
|
||||
Donations: [Donations]
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
UpdateDonations(goal: Int, progress: Int): Donations
|
||||
}
|
||||
18
backend/src/seed/factories/donations.js
Normal file
18
backend/src/seed/factories/donations.js
Normal file
@ -0,0 +1,18 @@
|
||||
import uuid from 'uuid/v4'
|
||||
|
||||
export default function create() {
|
||||
return {
|
||||
factory: async ({ args, neodeInstance }) => {
|
||||
const defaults = {
|
||||
id: uuid(),
|
||||
goal: 15000,
|
||||
progress: 0,
|
||||
}
|
||||
args = {
|
||||
...defaults,
|
||||
...args,
|
||||
}
|
||||
return neodeInstance.create('Donations', args)
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -8,6 +8,7 @@ import createTag from './tags.js'
|
||||
import createSocialMedia from './socialMedia.js'
|
||||
import createLocation from './locations.js'
|
||||
import createEmailAddress from './emailAddresses.js'
|
||||
import createDonations from './donations.js'
|
||||
import createUnverifiedEmailAddresss from './unverifiedEmailAddresses.js'
|
||||
|
||||
const factories = {
|
||||
@ -21,6 +22,7 @@ const factories = {
|
||||
Location: createLocation,
|
||||
EmailAddress: createEmailAddress,
|
||||
UnverifiedEmailAddress: createUnverifiedEmailAddresss,
|
||||
Donations: createDonations,
|
||||
}
|
||||
|
||||
export const cleanDatabase = async (options = {}) => {
|
||||
|
||||
@ -929,6 +929,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
|
||||
}),
|
||||
)
|
||||
|
||||
await factory.create('Donations')
|
||||
/* eslint-disable-next-line no-console */
|
||||
console.log('Seeded Data...')
|
||||
process.exit(0)
|
||||
|
||||
80
webapp/components/DonationInfo/DonationInfo.spec.js
Normal file
80
webapp/components/DonationInfo/DonationInfo.spec.js
Normal file
@ -0,0 +1,80 @@
|
||||
import { mount, createLocalVue } from '@vue/test-utils'
|
||||
import Styleguide from '@human-connection/styleguide'
|
||||
import DonationInfo from './DonationInfo.vue'
|
||||
|
||||
const localVue = createLocalVue()
|
||||
localVue.use(Styleguide)
|
||||
|
||||
const mockDate = new Date(2019, 11, 6)
|
||||
global.Date = jest.fn(() => mockDate)
|
||||
|
||||
describe('DonationInfo.vue', () => {
|
||||
let mocks, wrapper
|
||||
|
||||
beforeEach(() => {
|
||||
mocks = {
|
||||
$t: jest.fn(string => string),
|
||||
$i18n: {
|
||||
locale: () => 'de',
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const Wrapper = () => mount(DonationInfo, { mocks, localVue })
|
||||
|
||||
it('includes a link to the Human Connection donations website', () => {
|
||||
expect(
|
||||
Wrapper()
|
||||
.find('a')
|
||||
.attributes('href'),
|
||||
).toBe('https://human-connection.org/spenden/')
|
||||
})
|
||||
|
||||
it('displays a call to action button', () => {
|
||||
expect(
|
||||
Wrapper()
|
||||
.find('.ds-button')
|
||||
.text(),
|
||||
).toBe('donations.donate-now')
|
||||
})
|
||||
|
||||
it('creates a title from the current month and a translation string', () => {
|
||||
mocks.$t = jest.fn(() => 'Spenden für')
|
||||
expect(Wrapper().vm.title).toBe('Spenden für Dezember')
|
||||
})
|
||||
|
||||
describe('mount with data', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper()
|
||||
wrapper.setData({ goal: 50000, progress: 10000 })
|
||||
})
|
||||
|
||||
describe('given german locale', () => {
|
||||
it('creates a label from the given amounts and a translation string', () => {
|
||||
expect(mocks.$t).toBeCalledWith(
|
||||
'donations.amount-of-total',
|
||||
expect.objectContaining({
|
||||
amount: '10.000',
|
||||
total: '50.000',
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('given english locale', () => {
|
||||
beforeEach(() => {
|
||||
mocks.$i18n.locale = () => 'en'
|
||||
})
|
||||
|
||||
it('creates a label from the given amounts and a translation string', () => {
|
||||
expect(mocks.$t).toBeCalledWith(
|
||||
'donations.amount-of-total',
|
||||
expect.objectContaining({
|
||||
amount: '10,000',
|
||||
total: '50,000',
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
66
webapp/components/DonationInfo/DonationInfo.vue
Normal file
66
webapp/components/DonationInfo/DonationInfo.vue
Normal file
@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<div class="donation-info">
|
||||
<progress-bar :title="title" :label="label" :goal="goal" :progress="progress" />
|
||||
<a target="_blank" href="https://human-connection.org/spenden/">
|
||||
<ds-button primary>{{ $t('donations.donate-now') }}</ds-button>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { DonationsQuery } from '~/graphql/Donations'
|
||||
import ProgressBar from '~/components/ProgressBar/ProgressBar.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ProgressBar,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
goal: 15000,
|
||||
progress: 0,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
title() {
|
||||
const today = new Date()
|
||||
const month = today.toLocaleString(this.$i18n.locale(), { month: 'long' })
|
||||
return `${this.$t('donations.donations-for')} ${month}`
|
||||
},
|
||||
label() {
|
||||
return this.$t('donations.amount-of-total', {
|
||||
amount: this.progress.toLocaleString(this.$i18n.locale()),
|
||||
total: this.goal.toLocaleString(this.$i18n.locale()),
|
||||
})
|
||||
},
|
||||
},
|
||||
apollo: {
|
||||
Donations: {
|
||||
query() {
|
||||
return DonationsQuery()
|
||||
},
|
||||
update({ Donations }) {
|
||||
if (!Donations[0]) return
|
||||
const { goal, progress } = Donations[0]
|
||||
this.goal = goal
|
||||
this.progress = progress
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.donation-info {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
height: 100%;
|
||||
|
||||
@media (max-width: 546px) {
|
||||
width: 100%;
|
||||
height: 50%;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: $space-x-small;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
65
webapp/components/ProgressBar/ProgressBar.spec.js
Normal file
65
webapp/components/ProgressBar/ProgressBar.spec.js
Normal file
@ -0,0 +1,65 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import ProgressBar from './ProgressBar'
|
||||
|
||||
describe('ProgessBar.vue', () => {
|
||||
let propsData
|
||||
|
||||
beforeEach(() => {
|
||||
propsData = {
|
||||
goal: 50000,
|
||||
progress: 10000,
|
||||
}
|
||||
})
|
||||
|
||||
const Wrapper = () => mount(ProgressBar, { propsData })
|
||||
|
||||
describe('given only goal and progress', () => {
|
||||
it('renders no title', () => {
|
||||
expect(
|
||||
Wrapper()
|
||||
.find('.progress-bar__title')
|
||||
.exists(),
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('renders no label', () => {
|
||||
expect(
|
||||
Wrapper()
|
||||
.find('.progress-bar__label')
|
||||
.exists(),
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('calculates the progress bar width as a percentage of the goal', () => {
|
||||
expect(Wrapper().vm.progressBarWidth).toBe('width: 20%;')
|
||||
})
|
||||
})
|
||||
|
||||
describe('given a title', () => {
|
||||
beforeEach(() => {
|
||||
propsData.title = 'This is progress'
|
||||
})
|
||||
|
||||
it('renders the title', () => {
|
||||
expect(
|
||||
Wrapper()
|
||||
.find('.progress-bar__title')
|
||||
.text(),
|
||||
).toBe('This is progress')
|
||||
})
|
||||
})
|
||||
|
||||
describe('given a label', () => {
|
||||
beforeEach(() => {
|
||||
propsData.label = 'Going well'
|
||||
})
|
||||
|
||||
it('renders the label', () => {
|
||||
expect(
|
||||
Wrapper()
|
||||
.find('.progress-bar__label')
|
||||
.text(),
|
||||
).toBe('Going well')
|
||||
})
|
||||
})
|
||||
})
|
||||
97
webapp/components/ProgressBar/ProgressBar.vue
Normal file
97
webapp/components/ProgressBar/ProgressBar.vue
Normal file
@ -0,0 +1,97 @@
|
||||
<template>
|
||||
<div class="progress-bar">
|
||||
<div class="progress-bar__goal"></div>
|
||||
<div class="progress-bar__progress" :style="progressBarWidth"></div>
|
||||
<h4 v-if="title" class="progress-bar__title">{{ title }}</h4>
|
||||
<span v-if="label" class="progress-bar__label">{{ label }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
goal: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
},
|
||||
progress: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
progressBarWidth() {
|
||||
return `width: ${(this.progress / this.goal) * 100}%;`
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.progress-bar {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 240px;
|
||||
margin-right: $space-x-small;
|
||||
|
||||
@media (max-width: 680px) {
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
@media (max-width: 546px) {
|
||||
flex-basis: 50%;
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.progress-bar__title {
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
left: $space-xx-small;
|
||||
margin: 0;
|
||||
|
||||
@media (max-width: 546px) {
|
||||
top: $space-xx-small;
|
||||
}
|
||||
|
||||
@media (max-width: 350px) {
|
||||
font-size: $font-size-small;
|
||||
}
|
||||
}
|
||||
|
||||
.progress-bar__goal {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
height: 37.5px; // styleguide-button-size
|
||||
width: 100%;
|
||||
background-color: $color-neutral-100;
|
||||
border-radius: $border-radius-base;
|
||||
}
|
||||
|
||||
.progress-bar__progress {
|
||||
position: absolute;
|
||||
bottom: 1px;
|
||||
left: 0;
|
||||
height: 35.5px; // styleguide-button-size - 2px border
|
||||
max-width: 100%;
|
||||
background-color: $color-yellow;
|
||||
border-radius: $border-radius-base;
|
||||
}
|
||||
|
||||
.progress-bar__label {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: $space-xx-small;
|
||||
|
||||
@media (max-width: 350px) {
|
||||
font-size: $font-size-small;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
24
webapp/graphql/Donations.js
Normal file
24
webapp/graphql/Donations.js
Normal file
@ -0,0 +1,24 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const DonationsQuery = () => gql`
|
||||
query {
|
||||
Donations {
|
||||
id
|
||||
goal
|
||||
progress
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const UpdateDonations = () => {
|
||||
return gql`
|
||||
mutation($goal: Int, $progress: Int) {
|
||||
UpdateDonations(goal: $goal, progress: $progress) {
|
||||
id
|
||||
goal
|
||||
progress
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
@ -62,6 +62,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"donations": {
|
||||
"donations-for": "Spenden für",
|
||||
"donate-now": "Jetzt spenden",
|
||||
"amount-of-total": "{amount} von {total} € erreicht"
|
||||
},
|
||||
"maintenance": {
|
||||
"title": "Human Connection befindet sich in der Wartung",
|
||||
"explanation": "Zurzeit führen wir einige geplante Wartungsarbeiten durch, bitte versuch es später erneut.",
|
||||
@ -375,6 +380,12 @@
|
||||
"name": "Benutzer einladen",
|
||||
"title": "Leute einladen",
|
||||
"description": "Einladungen sind ein wunderbarer Weg, deine Freund in deinem Netzwerk zu haben …"
|
||||
},
|
||||
"donations": {
|
||||
"name": "Spendeninfo",
|
||||
"goal": "Monatlich benötigte Spenden",
|
||||
"progress": "Bereits gesammelte Spenden",
|
||||
"successfulUpdate": "Spenden-Info erfolgreich aktualisiert!"
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
|
||||
@ -63,6 +63,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"donations": {
|
||||
"donations-for": "Donations for",
|
||||
"donate-now": "Donate now",
|
||||
"amount-of-total": "{amount} of {total} € collected"
|
||||
},
|
||||
"maintenance": {
|
||||
"title": "Human Connection is under maintenance",
|
||||
"explanation": "At the moment we are doing some scheduled maintenance, please try again later.",
|
||||
@ -376,6 +381,12 @@
|
||||
"name": "Invite users",
|
||||
"title": "Invite people",
|
||||
"description": "Invitations are a wonderful way to have your friends in your network …"
|
||||
},
|
||||
"donations": {
|
||||
"name": "Donations info",
|
||||
"goal": "Monthly donations needed",
|
||||
"progress": "Donations collected so far",
|
||||
"successfulUpdate": "Donations info updated successfully!"
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
|
||||
@ -380,6 +380,12 @@
|
||||
"name": "Convidar usuários",
|
||||
"title": "Convidar pessoas",
|
||||
"description": "Convites são uma maneira maravilhosa de ter seus amigos em sua rede …"
|
||||
},
|
||||
"donations": {
|
||||
"name": "Informações sobre Doações",
|
||||
"goal": "Doações mensais necessárias",
|
||||
"progress": "Doações arrecadadas até o momento",
|
||||
"successfulUpdate": "Informações sobre doações atualizadas com sucesso!"
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
|
||||
@ -55,6 +55,10 @@ export default {
|
||||
name: this.$t('admin.invites.name'),
|
||||
path: `/admin/invite`,
|
||||
},
|
||||
{
|
||||
name: this.$t('admin.donations.name'),
|
||||
path: '/admin/donations',
|
||||
},
|
||||
// TODO implement
|
||||
/* {
|
||||
name: this.$t('admin.settings.name'),
|
||||
|
||||
63
webapp/pages/admin/donations.vue
Normal file
63
webapp/pages/admin/donations.vue
Normal file
@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<ds-card :header="$t('admin.donations.name')">
|
||||
<ds-form v-model="formData" @submit="submit">
|
||||
<ds-input model="goal" :label="$t('admin.donations.goal')" placeholder="15000" icon="money" />
|
||||
<ds-input
|
||||
model="progress"
|
||||
:label="$t('admin.donations.progress')"
|
||||
placeholder="1200"
|
||||
icon="money"
|
||||
/>
|
||||
<ds-button primary type="submit" :disabled="!formData.goal || !formData.progress">
|
||||
{{ $t('actions.save') }}
|
||||
</ds-button>
|
||||
</ds-form>
|
||||
</ds-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { DonationsQuery, UpdateDonations } from '~/graphql/Donations'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
formData: {
|
||||
goal: null,
|
||||
progress: null,
|
||||
},
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
submit() {
|
||||
const { goal, progress } = this.formData
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: UpdateDonations(),
|
||||
variables: {
|
||||
goal: parseInt(goal),
|
||||
progress: parseInt(progress),
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
this.$toast.success(this.$t('admin.donations.successfulUpdate'))
|
||||
})
|
||||
.catch(error => this.$toast.error(error.message))
|
||||
},
|
||||
},
|
||||
apollo: {
|
||||
Donations: {
|
||||
query() {
|
||||
return DonationsQuery()
|
||||
},
|
||||
update({ Donations }) {
|
||||
if (!Donations[0]) return
|
||||
const { goal, progress } = Donations[0]
|
||||
this.formData = {
|
||||
goal,
|
||||
progress,
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@ -64,6 +64,9 @@ describe('PostIndex', () => {
|
||||
truncate: a => a,
|
||||
removeLinks: jest.fn(),
|
||||
},
|
||||
$i18n: {
|
||||
locale: () => 'de',
|
||||
},
|
||||
// If you are mocking router, than don't use VueRouter with localVue: https://vue-test-utils.vuejs.org/guides/using-with-vue-router.html
|
||||
$router: {
|
||||
history: {
|
||||
|
||||
@ -4,7 +4,8 @@
|
||||
<ds-grid-item v-show="hashtag" :row-span="2" column-span="fullWidth">
|
||||
<filter-menu :hashtag="hashtag" @clearSearch="clearSearch" />
|
||||
</ds-grid-item>
|
||||
<ds-grid-item :row-span="2" column-span="fullWidth">
|
||||
<ds-grid-item :row-span="2" column-span="fullWidth" class="top-info-bar">
|
||||
<donation-info />
|
||||
<div class="sorting-dropdown">
|
||||
<ds-select
|
||||
v-model="selected"
|
||||
@ -57,6 +58,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DonationInfo from '~/components/DonationInfo/DonationInfo.vue'
|
||||
import FilterMenu from '~/components/FilterMenu/FilterMenu.vue'
|
||||
import HcEmpty from '~/components/Empty/Empty'
|
||||
import HcPostCard from '~/components/PostCard/PostCard.vue'
|
||||
@ -69,6 +71,7 @@ import PostMutations from '~/graphql/PostMutations'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
DonationInfo,
|
||||
FilterMenu,
|
||||
HcPostCard,
|
||||
HcLoadMore,
|
||||
@ -262,7 +265,20 @@ export default {
|
||||
.sorting-dropdown {
|
||||
width: 250px;
|
||||
position: relative;
|
||||
float: right;
|
||||
margin: 4px 0;
|
||||
|
||||
@media (max-width: 680px) {
|
||||
width: 180px;
|
||||
}
|
||||
}
|
||||
|
||||
.top-info-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
|
||||
@media (max-width: 546px) {
|
||||
grid-row-end: span 3 !important;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user