diff --git a/CHANGELOG.md b/CHANGELOG.md index 49fdfd07f..63b0c2c90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,55 @@ All notable changes to this project will be documented in this file. Dates are d Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). +#### [1.11.0](https://github.com/gradido/gradido/compare/1.10.1...1.11.0) + +- Fix navbar community item [`#2102`](https://github.com/gradido/gradido/pull/2102) +- Add validation date info to copied text after transaction link creation [`#2101`](https://github.com/gradido/gradido/pull/2101) +- Remove member area from menu (desktop and mobile), when user has no elopage account [`#2099`](https://github.com/gradido/gradido/pull/2099) +- fix: Use Inner Join for Contribution and User [`#2100`](https://github.com/gradido/gradido/pull/2100) +- Enable copying the link, username, amount, and memo text after transaction link creation [`#2098`](https://github.com/gradido/gradido/pull/2098) +- feat: Insert Missing Contributions Migration [`#2053`](https://github.com/gradido/gradido/pull/2053) +- [Bug] Wallet improvments for Contributions [`#2090`](https://github.com/gradido/gradido/pull/2090) +- [Fix] Add createdAt & contributionDate to ContributionListItems [`#2093`](https://github.com/gradido/gradido/pull/2093) +- fix: 🍰 Reset Amount In Contribution Form And Write A Test [`#2086`](https://github.com/gradido/gradido/pull/2086) +- [Feat] Replace logic to validation-provider. [`#2088`](https://github.com/gradido/gradido/pull/2088) +- fix: Add Confirm Dialog on Delete Contribution [`#2087`](https://github.com/gradido/gradido/pull/2087) +- fix: Admin Cannot Edit User Contribution [`#2085`](https://github.com/gradido/gradido/pull/2085) +- fix: Update contribution_date when Moved by Seed [`#2083`](https://github.com/gradido/gradido/pull/2083) +- chore: 🍰 Provide Volume For Backend Log-Files In Docker [`#2067`](https://github.com/gradido/gradido/pull/2067) +- feat: 🍰 Community Contribution Site And Form [`#2042`](https://github.com/gradido/gradido/pull/2042) +- [Refactor] Move MEMO_MIN_CHARS and MEMO_MAX_CHARS to const file [`#2082`](https://github.com/gradido/gradido/pull/2082) +- Fix: Test memo length on createContribution & updateContribution [`#2080`](https://github.com/gradido/gradido/pull/2080) +- chore: 🍰 Change `image` Entries In Docker Compose Files And Get Apple M1 Running [`#2050`](https://github.com/gradido/gradido/pull/2050) +- fix: Windows 0D 0A Linebreaks to Unix 0A [`#2064`](https://github.com/gradido/gradido/pull/2064) +- Add contributionDate to the Contribution object. [`#2066`](https://github.com/gradido/gradido/pull/2066) +- fix: Add Contributions to User [`#2062`](https://github.com/gradido/gradido/pull/2062) +- Feat: ContributionResolver - delete mutation [`#2035`](https://github.com/gradido/gradido/pull/2035) +- Fix: Add count to list contributions [`#2061`](https://github.com/gradido/gradido/pull/2061) +- docu: Explain how `.env` Files are Working [`#2022`](https://github.com/gradido/gradido/pull/2022) +- [WIP] 1794 feature event protocol 1 implement the basics of the business event protocol [`#1997`](https://github.com/gradido/gradido/pull/1997) +- docs: 🍰 Document The Setup Of The GraphQL Playground [`#2060`](https://github.com/gradido/gradido/pull/2060) +- Feat: List all contribution [`#2057`](https://github.com/gradido/gradido/pull/2057) +- feat: Do not log IntrospectionQuery from Query Browser [`#2059`](https://github.com/gradido/gradido/pull/2059) +- Feat: Add confirmedBy and confirmedAt for the contribution query. [`#2052`](https://github.com/gradido/gradido/pull/2052) +- Add open creations to webapp [`#2048`](https://github.com/gradido/gradido/pull/2048) +- Prevent session expiration modal from displaying negative seconds, when session is expired for more than 0 seconds [`#2054`](https://github.com/gradido/gradido/pull/2054) +- feat: mutation contribution update [`#2032`](https://github.com/gradido/gradido/pull/2032) +- feat: Login Returns Open Creations for User [`#2046`](https://github.com/gradido/gradido/pull/2046) +- Migrate transaction to valid dataset for gradido node [`#2029`](https://github.com/gradido/gradido/pull/2029) +- feat: implement contribution list query [`#2031`](https://github.com/gradido/gradido/pull/2031) +- add code for moving user creation date if transaction before exist [`#2034`](https://github.com/gradido/gradido/pull/2034) +- change text from page [`#2037`](https://github.com/gradido/gradido/pull/2037) +- Transaction link: copy link, text and more [`#2030`](https://github.com/gradido/gradido/pull/2030) +- change welcome in community text [`#2025`](https://github.com/gradido/gradido/pull/2025) +- changed link color in navbar and language switch [`#2024`](https://github.com/gradido/gradido/pull/2024) +- feat: ContributionResolver - createContribution [`#2009`](https://github.com/gradido/gradido/pull/2009) + #### [1.10.1](https://github.com/gradido/gradido/compare/1.10.0...1.10.1) +> 30 June 2022 + +- release: 1.10.1 [`#2021`](https://github.com/gradido/gradido/pull/2021) - automatic session logout with info modal [`#2001`](https://github.com/gradido/gradido/pull/2001) - 1910 separate text for the slideshow images. [`#1998`](https://github.com/gradido/gradido/pull/1998) - Origin/1921 additional parameter checks for createContributionLinks [`#1996`](https://github.com/gradido/gradido/pull/1996) diff --git a/DOCKER_MORE_CLOSELY.md b/DOCKER_MORE_CLOSELY.md new file mode 100644 index 000000000..f2aae81c7 --- /dev/null +++ b/DOCKER_MORE_CLOSELY.md @@ -0,0 +1,44 @@ +# Docker More Closely + +## Apple M1 Platform + +***Attention:** For using Docker commands in Apple M1 environments!* + +### Enviroment Variable For Apple M1 Platform + +To set the Docker platform environment variable in your terminal tab, run: + +```bash +# set env variable for your shell +$ export DOCKER_DEFAULT_PLATFORM=linux/amd64 +``` + +### Docker Compose Override File For Apple M1 Platform + +For Docker compose `up` or `build` commands, you can use our Apple M1 override file that specifies the M1 platform: + +```bash +# in main folder + +# for development +$ docker compose -f docker-compose.yml -f docker-compose.override.yml -f docker-compose.apple-m1.override.yml up + +# for production +$ docker compose -f docker-compose.yml -f docker-compose.apple-m1.override.yml up +``` + +## Analysing Docker Builds + +To analyze a Docker build, there is a wonderful tool called [dive](https://github.com/wagoodman/dive). Please sponsor if you're using it! + +The `dive build` command is exactly the right one to fulfill what we are looking for. +We can use it just like the `docker build` command and get an analysis afterwards. + +So, in our main folder, we use it in the following way: + +```bash +# in main folder +$ dive build --target -t "gradido/:local-" / +``` + +For the specific applications, see our [publish.yml](.github/workflows/publish.yml). diff --git a/README.md b/README.md index 289a39109..3d086018e 100644 --- a/README.md +++ b/README.md @@ -121,6 +121,19 @@ After generating a new version you should commit the changes. This will be the C Note: The Changelog will be regenerated with all tags on release on the external builder tool, but will not be checked in there. The Changelog on the github release will therefore always be correct, on the repo it might be incorrect due to missing tags when executing the `yarn release` command. +## How the different .env work on deploy + +Each component (frontend, admin, backend and database) has its own `.env` file. When running in development with docker and nginx you usually do not have to care about the `.env`. The defaults are set by the respective config file, found in the `src/config/` folder of each component. But if you have a local `.env`, the defaults set in the config are overwritten by the `.env`. If you do not use docker, you need the `.env` in the frontend and admin interface because nginx is not running in order to find the backend. + +Each component has a `.env.dist` file. This file contains all environment variables used by the component and can be used as pattern. If you want to use a local `.env`, copy the `.env.dist` and adjust the variables accordingly. + +Each component has a `.env.template` file. These files are very important on deploy. + +There is one `.env.dist` in the `deployment/bare_metal/` folder. This `.env.dist` contains all variables used by the components, e.g. unites all `.env.dist` from the components. On deploy, we copy this `.env.dist` to `.env` and set all variables in this new file. The deploy script loads this variables and provides them by the `.env.templates` of each component, creating an `.env` for each component (see in `deployment/bare_metal/start.sh` the `envsubst`). + +To avoid forgetting to update an existing `.env` in the `deployment/bare_metal/` folder when deploying, we have an environment version variable inside the codebase of each component. You should update this version, when environment variables must be changed or added on deploy. The code checks, that the environement version provided by the `.env` is the one expected by the codebase. + + ## Troubleshooting | Problem | Issue | Solution | Description | diff --git a/admin/jest.config.js b/admin/jest.config.js index 9b9842bad..9233dd2e7 100644 --- a/admin/jest.config.js +++ b/admin/jest.config.js @@ -26,5 +26,5 @@ module.exports = { testMatch: ['**/?(*.)+(spec|test).js?(x)'], // snapshotSerializers: ['jest-serializer-vue'], transformIgnorePatterns: ['/node_modules/(?!vee-validate/dist/rules)'], - testEnvironment: 'jest-environment-jsdom-sixteen', + testEnvironment: 'jest-environment-jsdom-sixteen', // why this is still needed? should not be needed anymore since jest@26, see: https://www.npmjs.com/package/jest-environment-jsdom-sixteen } diff --git a/admin/package.json b/admin/package.json index 50145d44a..9879064de 100644 --- a/admin/package.json +++ b/admin/package.json @@ -3,7 +3,7 @@ "description": "Administraion Interface for Gradido", "main": "index.js", "author": "Moriz Wahl", - "version": "1.10.1", + "version": "1.11.0", "license": "Apache-2.0", "private": false, "scripts": { @@ -39,6 +39,7 @@ "identity-obj-proxy": "^3.0.0", "jest": "26.6.3", "jest-canvas-mock": "^2.3.1", + "jest-environment-jsdom-sixteen": "^2.0.0", "portal-vue": "^2.1.7", "qrcanvas-vue": "2.1.1", "regenerator-runtime": "^0.13.9", @@ -70,7 +71,6 @@ "eslint-plugin-prettier": "3.3.1", "eslint-plugin-promise": "^5.1.1", "eslint-plugin-vue": "^7.20.0", - "jest-environment-jsdom-sixteen": "^2.0.0", "postcss": "^8.4.8", "postcss-html": "^1.3.0", "postcss-scss": "^4.0.3", diff --git a/admin/src/components/CommunityStatistic.spec.js b/admin/src/components/CommunityStatistic.spec.js new file mode 100644 index 000000000..dbcca5fed --- /dev/null +++ b/admin/src/components/CommunityStatistic.spec.js @@ -0,0 +1,39 @@ +import { mount } from '@vue/test-utils' +import CommunityStatistic from './CommunityStatistic' + +const localVue = global.localVue + +const mocks = { + $t: jest.fn((t) => t), + $n: jest.fn((n) => n), +} + +const propsData = { + value: { + totalUsers: '123', + activeUsers: '100', + deletedUsers: '5', + totalGradidoCreated: '2500', + totalGradidoDecayed: '200', + totalGradidoAvailable: '500', + totalGradidoUnbookedDecayed: '111', + }, +} + +describe('CommunityStatistic', () => { + let wrapper + + const Wrapper = () => { + return mount(CommunityStatistic, { localVue, mocks, propsData }) + } + + describe('mount', () => { + beforeEach(() => { + wrapper = Wrapper() + }) + + it('renders the Div Element ".community-statistic"', () => { + expect(wrapper.find('div.community-statistic').exists()).toBe(true) + }) + }) +}) diff --git a/admin/src/components/CommunityStatistic.vue b/admin/src/components/CommunityStatistic.vue new file mode 100644 index 000000000..c19f8deec --- /dev/null +++ b/admin/src/components/CommunityStatistic.vue @@ -0,0 +1,59 @@ + + diff --git a/admin/src/components/ContentFooter.spec.js b/admin/src/components/ContentFooter.spec.js new file mode 100644 index 000000000..b7b8d5aef --- /dev/null +++ b/admin/src/components/ContentFooter.spec.js @@ -0,0 +1,29 @@ +import { mount } from '@vue/test-utils' +import ContentFooter from './ContentFooter' + +const localVue = global.localVue + +const mocks = { + $t: jest.fn((t) => t), + $i18n: { + locale: jest.fn(() => 'en'), + }, +} + +describe('ContentFooter', () => { + let wrapper + + const Wrapper = () => { + return mount(ContentFooter, { localVue, mocks }) + } + + describe('mount', () => { + beforeEach(() => { + wrapper = Wrapper() + }) + + it('renders the div element ".content-footer"', () => { + expect(wrapper.find('div.content-footer').exists()).toBe(true) + }) + }) +}) diff --git a/admin/src/components/ContentFooter.vue b/admin/src/components/ContentFooter.vue index c10e53596..bab3f5d12 100644 --- a/admin/src/components/ContentFooter.vue +++ b/admin/src/components/ContentFooter.vue @@ -1,5 +1,5 @@ - diff --git a/admin/src/graphql/communityStatistics.js b/admin/src/graphql/communityStatistics.js new file mode 100644 index 000000000..868bfd02a --- /dev/null +++ b/admin/src/graphql/communityStatistics.js @@ -0,0 +1,15 @@ +import gql from 'graphql-tag' + +export const communityStatistics = gql` + query { + communityStatistics { + totalUsers + activeUsers + deletedUsers + totalGradidoCreated + totalGradidoDecayed + totalGradidoAvailable + totalGradidoUnbookedDecayed + } + } +` diff --git a/admin/src/locales/de.json b/admin/src/locales/de.json index fa0ca6903..4f1b40b71 100644 --- a/admin/src/locales/de.json +++ b/admin/src/locales/de.json @@ -35,6 +35,7 @@ "creation_form": { "creation_failed": "Ausstehende Schöpfung für {email} konnte nicht erzeugt werden.", "creation_for": "Aktives Grundeinkommen für", + "deleteNow": "Möchtest du diesen Beitrag zur Gemeinschaft wirklich löschen?", "enter_text": "Text eintragen", "form": "Schöpfungsformular", "min_characters": "Mindestens 10 Zeichen eingeben", @@ -72,6 +73,7 @@ "hide_details": "Details verbergen", "lastname": "Nachname", "math": { + "colon": ":", "exclaim": "!", "pipe": "|", "plus": "+" @@ -104,6 +106,16 @@ "removeNotSelf": "Als Admin/Moderator kannst du dich nicht selber löschen.", "remove_all": "alle Nutzer entfernen", "save": "Speichern", + "statistic": { + "activeUsers": "Aktive Mitglieder", + "deletedUsers": "Gelöschte Mitglieder", + "name": "Statistik", + "totalGradidoAvailable": "GDD insgesamt im Umlauf", + "totalGradidoCreated": "GDD insgesamt geschöpft", + "totalGradidoDecayed": "GDD insgesamt verfallen", + "totalGradidoUnbookedDecayed": "Ungebuchter GDD Verfall", + "totalUsers": "Mitglieder" + }, "status": "Status", "success": "Erfolg", "text": "Text", diff --git a/admin/src/locales/en.json b/admin/src/locales/en.json index 6d19b1732..2145a87d3 100644 --- a/admin/src/locales/en.json +++ b/admin/src/locales/en.json @@ -35,6 +35,7 @@ "creation_form": { "creation_failed": "Could not create pending creation for {email}", "creation_for": "Active Basic Income for", + "deleteNow": "Do you really want to delete this contribution to the community?", "enter_text": "Enter text", "form": "Creation form", "min_characters": "Enter at least 10 characters", @@ -72,6 +73,7 @@ "hide_details": "Hide details", "lastname": "Lastname", "math": { + "colon": ":", "exclaim": "!", "pipe": "|", "plus": "+" @@ -104,6 +106,16 @@ "removeNotSelf": "As an admin/moderator, you cannot delete yourself.", "remove_all": "Remove all users", "save": "Speichern", + "statistic": { + "activeUsers": "Active members", + "deletedUsers": "Deleted members", + "name": "Statistic", + "totalGradidoAvailable": "Total GDD in circulation", + "totalGradidoCreated": "Total created GDD", + "totalGradidoDecayed": "Total GDD decay", + "totalGradidoUnbookedDecayed": "Unbooked GDD decay", + "totalUsers": "Members" + }, "status": "Status", "success": "Success", "text": "Text", diff --git a/admin/src/pages/Creation.vue b/admin/src/pages/Creation.vue index 9e554ff92..26d44fd3e 100644 --- a/admin/src/pages/Creation.vue +++ b/admin/src/pages/Creation.vue @@ -29,6 +29,7 @@ per-page="perPage" :total-rows="rows" align="center" + :hide-ellipsis="true" > diff --git a/admin/src/pages/CreationConfirm.spec.js b/admin/src/pages/CreationConfirm.spec.js index 632f19ff9..352eba809 100644 --- a/admin/src/pages/CreationConfirm.spec.js +++ b/admin/src/pages/CreationConfirm.spec.js @@ -18,7 +18,7 @@ const apolloQueryMock = jest.fn().mockResolvedValue({ amount: 500, memo: 'Danke für alles', date: new Date(), - moderator: 0, + moderator: 2, }, { id: 2, @@ -28,7 +28,7 @@ const apolloQueryMock = jest.fn().mockResolvedValue({ amount: 1000000, memo: 'Gut Ergattert', date: new Date(), - moderator: 0, + moderator: 2, }, ], }, @@ -80,28 +80,54 @@ describe('CreationConfirm', () => { }) describe('remove creation with success', () => { - beforeEach(async () => { - await wrapper.findAll('tr').at(1).findAll('button').at(0).trigger('click') - }) + let spy - it('calls the adminDeleteContribution mutation', () => { - expect(apolloMutateMock).toBeCalledWith({ - mutation: adminDeleteContribution, - variables: { id: 1 }, + describe('admin confirms deletion', () => { + beforeEach(async () => { + spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm') + spy.mockImplementation(() => Promise.resolve('some value')) + await wrapper.findAll('tr').at(1).findAll('button').at(0).trigger('click') + }) + + it('opens a modal', () => { + expect(spy).toBeCalled() + }) + + it('calls the adminDeleteContribution mutation', () => { + expect(apolloMutateMock).toBeCalledWith({ + mutation: adminDeleteContribution, + variables: { id: 1 }, + }) + }) + + it('commits openCreationsMinus to store', () => { + expect(storeCommitMock).toBeCalledWith('openCreationsMinus', 1) + }) + + it('toasts a success message', () => { + expect(toastSuccessSpy).toBeCalledWith('creation_form.toasted_delete') }) }) - it('commits openCreationsMinus to store', () => { - expect(storeCommitMock).toBeCalledWith('openCreationsMinus', 1) - }) + describe('admin cancels deletion', () => { + beforeEach(async () => { + spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm') + spy.mockImplementation(() => Promise.resolve(false)) + await wrapper.findAll('tr').at(1).findAll('button').at(0).trigger('click') + }) - it('toasts a success message', () => { - expect(toastSuccessSpy).toBeCalledWith('creation_form.toasted_delete') + it('does not call the adminDeleteContribution mutation', () => { + expect(apolloMutateMock).not.toBeCalled() + }) }) }) describe('remove creation with error', () => { + let spy + beforeEach(async () => { + spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm') + spy.mockImplementation(() => Promise.resolve('some value')) apolloMutateMock.mockRejectedValue({ message: 'Ouchhh!' }) await wrapper.findAll('tr').at(1).findAll('button').at(0).trigger('click') }) diff --git a/admin/src/pages/CreationConfirm.vue b/admin/src/pages/CreationConfirm.vue index 061556ba1..c07e6b351 100644 --- a/admin/src/pages/CreationConfirm.vue +++ b/admin/src/pages/CreationConfirm.vue @@ -34,20 +34,23 @@ export default { }, methods: { removeCreation(item) { - this.$apollo - .mutate({ - mutation: adminDeleteContribution, - variables: { - id: item.id, - }, - }) - .then((result) => { - this.updatePendingCreations(item.id) - this.toastSuccess(this.$t('creation_form.toasted_delete')) - }) - .catch((error) => { - this.toastError(error.message) - }) + this.$bvModal.msgBoxConfirm(this.$t('creation_form.deleteNow')).then(async (value) => { + if (value) + await this.$apollo + .mutate({ + mutation: adminDeleteContribution, + variables: { + id: item.id, + }, + }) + .then((result) => { + this.updatePendingCreations(item.id) + this.toastSuccess(this.$t('creation_form.toasted_delete')) + }) + .catch((error) => { + this.toastError(error.message) + }) + }) }, confirmCreation() { this.$apollo @@ -114,7 +117,7 @@ export default { }, }, { key: 'moderator', label: this.$t('moderator') }, - { key: 'edit_creation', label: this.$t('edit') }, + { key: 'editCreation', label: this.$t('edit') }, { key: 'confirm', label: this.$t('save') }, ] }, diff --git a/admin/src/pages/Overview.spec.js b/admin/src/pages/Overview.spec.js index cc5a0c3a5..e0063a446 100644 --- a/admin/src/pages/Overview.spec.js +++ b/admin/src/pages/Overview.spec.js @@ -1,28 +1,83 @@ import { mount } from '@vue/test-utils' import Overview from './Overview.vue' +import { listContributionLinks } from '@/graphql/listContributionLinks.js' +import { communityStatistics } from '@/graphql/communityStatistics.js' +import { listUnconfirmedContributions } from '@/graphql/listUnconfirmedContributions.js' const localVue = global.localVue -const apolloQueryMock = jest.fn().mockResolvedValue({ - data: { - listUnconfirmedContributions: [ - { - pending: true, +const apolloQueryMock = jest + .fn() + .mockResolvedValueOnce({ + data: { + listUnconfirmedContributions: [ + { + pending: true, + }, + { + pending: true, + }, + { + pending: true, + }, + ], + }, + }) + .mockResolvedValueOnce({ + data: { + communityStatistics: { + totalUsers: 3113, + activeUsers: 1057, + deletedUsers: 35, + totalGradidoCreated: '4083774.05000000000000000000', + totalGradidoDecayed: '-1062639.13634129622923372197', + totalGradidoAvailable: '2513565.869444365732411569', + totalGradidoUnbookedDecayed: '-500474.6738366222166261272', }, - { - pending: true, + }, + }) + .mockResolvedValueOnce({ + data: { + listContributionLinks: { + links: [ + { + id: 1, + name: 'Meditation', + memo: 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut l', + amount: '200', + validFrom: '2022-04-01', + validTo: '2022-08-01', + cycle: 'täglich', + maxPerCycle: '3', + maxAmountPerMonth: 0, + link: 'https://localhost/redeem/CL-1a2345678', + }, + ], + count: 1, }, - { - pending: true, - }, - ], - }, -}) + }, + }) + .mockResolvedValue({ + data: { + listUnconfirmedContributions: [ + { + pending: true, + }, + { + pending: true, + }, + { + pending: true, + }, + ], + }, + }) const storeCommitMock = jest.fn() const mocks = { $t: jest.fn((t) => t), + $n: jest.fn((n) => n), $apollo: { query: apolloQueryMock, }, @@ -47,10 +102,30 @@ describe('Overview', () => { }) it('calls listUnconfirmedContributions', () => { - expect(apolloQueryMock).toBeCalled() + expect(apolloQueryMock).toBeCalledWith( + expect.objectContaining({ + query: listUnconfirmedContributions, + }), + ) }) - it('commts three pending creations to store', () => { + it('calls communityStatistics', () => { + expect(apolloQueryMock).toBeCalledWith( + expect.objectContaining({ + query: communityStatistics, + }), + ) + }) + + it('calls listContributionLinks', () => { + expect(apolloQueryMock).toBeCalledWith( + expect.objectContaining({ + query: listContributionLinks, + }), + ) + }) + + it('commits three pending creations to store', () => { expect(storeCommitMock).toBeCalledWith('setOpenCreations', 3) }) diff --git a/admin/src/pages/Overview.vue b/admin/src/pages/Overview.vue index 69aa15c1a..dcef93a14 100644 --- a/admin/src/pages/Overview.vue +++ b/admin/src/pages/Overview.vue @@ -29,26 +29,39 @@ + diff --git a/frontend/src/components/Contributions/ContributionList.spec.js b/frontend/src/components/Contributions/ContributionList.spec.js new file mode 100644 index 000000000..a1dfc934d --- /dev/null +++ b/frontend/src/components/Contributions/ContributionList.spec.js @@ -0,0 +1,120 @@ +import { mount } from '@vue/test-utils' +import ContributionList from './ContributionList.vue' + +const localVue = global.localVue + +describe('ContributionList', () => { + let wrapper + + const mocks = { + $t: jest.fn((t) => t), + $d: jest.fn((d) => d), + } + + const propsData = { + contributionCount: 3, + showPagination: true, + pageSize: 25, + items: [ + { + id: 0, + date: '07/06/2022', + memo: 'Ich habe 10 Stunden die Elbwiesen von Müll befreit.', + amount: '200', + }, + { + id: 1, + date: '06/22/2022', + memo: 'Ich habe 30 Stunden Frau Müller beim EInkaufen und im Haushalt geholfen.', + amount: '600', + }, + { + id: 2, + date: '05/04/2022', + memo: + 'Ich habe 50 Stunden den Nachbarkindern bei ihren Hausaufgaben geholfen und Nachhilfeunterricht gegeben.', + amount: '1000', + }, + ], + } + + const Wrapper = () => { + return mount(ContributionList, { + localVue, + mocks, + propsData, + }) + } + + describe('mount', () => { + beforeEach(() => { + wrapper = Wrapper() + }) + + it('has a DIV .contribution-list', () => { + expect(wrapper.find('div.contribution-list').exists()).toBe(true) + }) + + describe('pagination', () => { + describe('list count smaller than page size', () => { + it('has no pagination buttons', () => { + expect(wrapper.find('ul.pagination').exists()).toBe(false) + }) + }) + + describe('list count greater than page size', () => { + beforeEach(() => { + wrapper.setProps({ contributionCount: 33 }) + }) + + it('has pagination buttons', () => { + expect(wrapper.find('ul.pagination').exists()).toBe(true) + }) + }) + + describe('switch page', () => { + const scrollToMock = jest.fn() + window.scrollTo = scrollToMock + + beforeEach(async () => { + await wrapper.setProps({ contributionCount: 33 }) + wrapper.findComponent({ name: 'BPagination' }).vm.$emit('input', 2) + }) + + it('emits update contribution list', () => { + expect(wrapper.emitted('update-list-contributions')).toEqual([ + [{ currentPage: 2, pageSize: 25 }], + ]) + }) + + it('scrolls to top', () => { + expect(scrollToMock).toBeCalledWith(0, 0) + }) + }) + }) + + describe('update contribution', () => { + beforeEach(() => { + wrapper + .findComponent({ name: 'ContributionListItem' }) + .vm.$emit('update-contribution-form', 'item') + }) + + it('emits update contribution form', () => { + expect(wrapper.emitted('update-contribution-form')).toEqual([['item']]) + }) + }) + + describe('delete contribution', () => { + beforeEach(() => { + wrapper + .findComponent({ name: 'ContributionListItem' }) + .vm.$emit('delete-contribution', { id: 2 }) + }) + + it('emits delete contribution', () => { + expect(wrapper.emitted('delete-contribution')).toEqual([[{ id: 2 }]]) + }) + }) + }) +}) diff --git a/frontend/src/components/Contributions/ContributionList.vue b/frontend/src/components/Contributions/ContributionList.vue new file mode 100644 index 000000000..cf8606e33 --- /dev/null +++ b/frontend/src/components/Contributions/ContributionList.vue @@ -0,0 +1,77 @@ + + diff --git a/frontend/src/components/Contributions/ContributionListItem.spec.js b/frontend/src/components/Contributions/ContributionListItem.spec.js new file mode 100644 index 000000000..59918e762 --- /dev/null +++ b/frontend/src/components/Contributions/ContributionListItem.spec.js @@ -0,0 +1,145 @@ +import { mount } from '@vue/test-utils' +import ContributionListItem from './ContributionListItem.vue' + +const localVue = global.localVue + +describe('ContributionListItem', () => { + let wrapper + + const mocks = { + $t: jest.fn((t) => t), + $d: jest.fn((d) => d), + } + + const propsData = { + id: 1, + createdAt: '26/07/2022', + contributionDate: '07/06/2022', + memo: 'Ich habe 10 Stunden die Elbwiesen von Müll befreit.', + amount: '200', + } + + const Wrapper = () => { + return mount(ContributionListItem, { + localVue, + mocks, + propsData, + }) + } + + describe('mount', () => { + beforeEach(() => { + jest.clearAllMocks() + wrapper = Wrapper() + }) + + it('has a DIV .contribution-list-item', () => { + expect(wrapper.find('div.contribution-list-item').exists()).toBe(true) + }) + + describe('contribution type', () => { + it('is pending by default', () => { + expect(wrapper.vm.type).toBe('pending') + }) + + it('is deleted when deletedAt is present', async () => { + await wrapper.setProps({ deletedAt: new Date().toISOString() }) + expect(wrapper.vm.type).toBe('deleted') + }) + + it('is confirmed when confirmedAt is present', async () => { + await wrapper.setProps({ confirmedAt: new Date().toISOString() }) + expect(wrapper.vm.type).toBe('confirmed') + }) + }) + + describe('contribution icon', () => { + it('is bell-fill by default', () => { + expect(wrapper.vm.icon).toBe('bell-fill') + }) + + it('is x-circle when deletedAt is present', async () => { + await wrapper.setProps({ deletedAt: new Date().toISOString() }) + expect(wrapper.vm.icon).toBe('x-circle') + }) + + it('is check when confirmedAt is present', async () => { + await wrapper.setProps({ confirmedAt: new Date().toISOString() }) + expect(wrapper.vm.icon).toBe('check') + }) + }) + + describe('contribution variant', () => { + it('is primary by default', () => { + expect(wrapper.vm.variant).toBe('primary') + }) + + it('is danger when deletedAt is present', async () => { + await wrapper.setProps({ deletedAt: new Date().toISOString() }) + expect(wrapper.vm.variant).toBe('danger') + }) + + it('is success at when confirmedAt is present', async () => { + await wrapper.setProps({ confirmedAt: new Date().toISOString() }) + expect(wrapper.vm.variant).toBe('success') + }) + }) + + describe('date', () => { + it('is equal to createdAt', () => { + expect(wrapper.vm.date).toBe(wrapper.vm.createdAt) + }) + }) + + describe('delete contribution', () => { + let spy + + describe('edit contribution', () => { + beforeEach(() => { + wrapper.findAll('div.pointer').at(0).trigger('click') + }) + + it('emits update contribution form', () => { + expect(wrapper.emitted('update-contribution-form')).toEqual([ + [ + { + id: 1, + contributionDate: '07/06/2022', + memo: 'Ich habe 10 Stunden die Elbwiesen von Müll befreit.', + amount: '200', + }, + ], + ]) + }) + }) + + describe('confirm deletion', () => { + beforeEach(() => { + spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm') + spy.mockImplementation(() => Promise.resolve(true)) + wrapper.findAll('div.pointer').at(1).trigger('click') + }) + + it('opens the modal', () => { + expect(spy).toBeCalledWith('contribution.delete') + }) + + it('emits delete contribution', () => { + expect(wrapper.emitted('delete-contribution')).toEqual([[{ id: 1 }]]) + }) + }) + + describe('cancel deletion', () => { + beforeEach(async () => { + spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm') + spy.mockImplementation(() => Promise.resolve(false)) + await wrapper.findAll('div.pointer').at(1).trigger('click') + }) + + it('does not emit delete contribution', () => { + expect(wrapper.emitted('delete-contribution')).toBeFalsy() + }) + }) + }) + }) +}) diff --git a/frontend/src/components/Contributions/ContributionListItem.vue b/frontend/src/components/Contributions/ContributionListItem.vue new file mode 100644 index 000000000..90e98bc4c --- /dev/null +++ b/frontend/src/components/Contributions/ContributionListItem.vue @@ -0,0 +1,114 @@ + + diff --git a/frontend/src/components/GddSend/TransactionConfirmationLink.spec.js b/frontend/src/components/GddSend/TransactionConfirmationLink.spec.js index a28c2d185..91207901c 100644 --- a/frontend/src/components/GddSend/TransactionConfirmationLink.spec.js +++ b/frontend/src/components/GddSend/TransactionConfirmationLink.spec.js @@ -46,5 +46,17 @@ describe('GddSend confirm', () => { expect(wrapper.findAll('div.confirm-box-link').at(0).exists()).toBeTruthy() }) }) + + describe('has totalBalance under 0', () => { + beforeEach(async () => { + await wrapper.setProps({ + balance: 0, + }) + }) + + it('has send button disabled', () => { + expect(wrapper.find('.send-button').attributes('disabled')).toBe('disabled') + }) + }) }) }) diff --git a/frontend/src/components/GddSend/TransactionConfirmationLink.vue b/frontend/src/components/GddSend/TransactionConfirmationLink.vue index c23ed35d2..2620753ee 100644 --- a/frontend/src/components/GddSend/TransactionConfirmationLink.vue +++ b/frontend/src/components/GddSend/TransactionConfirmationLink.vue @@ -42,7 +42,12 @@ {{ $t('back') }} - + {{ $t('form.generate_now') }} diff --git a/frontend/src/components/GddSend/TransactionResultLink.vue b/frontend/src/components/GddSend/TransactionResultLink.vue index 04445acfe..2e1e68ac0 100644 --- a/frontend/src/components/GddSend/TransactionResultLink.vue +++ b/frontend/src/components/GddSend/TransactionResultLink.vue @@ -3,7 +3,13 @@
{{ $t('gdd_per_link.created') }}
- +
@@ -27,10 +33,10 @@ export default { FigureQrCode, }, props: { - link: { - type: String, - required: true, - }, + link: { type: String, required: true }, + amount: { type: String, required: true }, + memo: { type: String, required: true }, + validUntil: { type: String, required: true }, }, data() { return { diff --git a/frontend/src/components/GddTransactionList.vue b/frontend/src/components/GddTransactionList.vue index 5becfa39e..a74be5187 100644 --- a/frontend/src/components/GddTransactionList.vue +++ b/frontend/src/components/GddTransactionList.vue @@ -69,6 +69,7 @@ :per-page="pageSize" :total-rows="transactionCount" align="center" + :hide-ellipsis="true" >
diff --git a/frontend/src/components/GdtTransactionList.vue b/frontend/src/components/GdtTransactionList.vue index 4934f9fce..f915cd881 100644 --- a/frontend/src/components/GdtTransactionList.vue +++ b/frontend/src/components/GdtTransactionList.vue @@ -36,6 +36,7 @@ :per-page="pageSize" :total-rows="transactionGdtCount" align="center" + :hide-ellipsis="true" >
diff --git a/frontend/src/components/LanguageSwitch.spec.js b/frontend/src/components/LanguageSwitch.spec.js index cf7c4a35e..a6abc2359 100644 --- a/frontend/src/components/LanguageSwitch.spec.js +++ b/frontend/src/components/LanguageSwitch.spec.js @@ -45,7 +45,7 @@ describe('LanguageSwitch', () => { expect(wrapper.find('div.language-switch').exists()).toBeTruthy() }) - describe('with locales en and de', () => { + describe('with locales en, de and es', () => { describe('empty store', () => { describe('navigator language is "en-US"', () => { const languageGetter = jest.spyOn(navigator, 'language', 'get') @@ -69,11 +69,44 @@ describe('LanguageSwitch', () => { }) }) - describe('navigator language is "es-ES" (not supported)', () => { + describe('navigator language is "es-ES"', () => { + const languageGetter = jest.spyOn(navigator, 'language', 'get') + + it('shows Español as language ', async () => { + languageGetter.mockReturnValue('es-ES') + wrapper.vm.setCurrentLanguage() + await wrapper.vm.$nextTick() + expect(wrapper.find('button.dropdown-toggle').text()).toBe('Español - es') + }) + }) + + describe('navigator language is "fr-FR"', () => { + const languageGetter = jest.spyOn(navigator, 'language', 'get') + + it('shows French as language ', async () => { + languageGetter.mockReturnValue('fr-FR') + wrapper.vm.setCurrentLanguage() + await wrapper.vm.$nextTick() + expect(wrapper.find('button.dropdown-toggle').text()).toBe('Français - fr') + }) + }) + + describe('navigator language is "nl-NL"', () => { + const languageGetter = jest.spyOn(navigator, 'language', 'get') + + it('shows Dutch as language ', async () => { + languageGetter.mockReturnValue('nl-NL') + wrapper.vm.setCurrentLanguage() + await wrapper.vm.$nextTick() + expect(wrapper.find('button.dropdown-toggle').text()).toBe('Holandés - nl') + }) + }) + + describe('navigator language is "it-IT" (not supported)', () => { const languageGetter = jest.spyOn(navigator, 'language', 'get') it('shows English as language ', async () => { - languageGetter.mockReturnValue('es-ES') + languageGetter.mockReturnValue('it-IT') wrapper.vm.setCurrentLanguage() await wrapper.vm.$nextTick() expect(wrapper.find('button.dropdown-toggle').text()).toBe('English - en') @@ -101,9 +134,36 @@ describe('LanguageSwitch', () => { }) }) + describe('language "es" in store', () => { + it('shows Español as language', async () => { + wrapper.vm.$store.state.language = 'es' + wrapper.vm.setCurrentLanguage() + await wrapper.vm.$nextTick() + expect(wrapper.find('button.dropdown-toggle').text()).toBe('Español - es') + }) + }) + + describe('language "fr" in store', () => { + it('shows French as language', async () => { + wrapper.vm.$store.state.language = 'fr' + wrapper.vm.setCurrentLanguage() + await wrapper.vm.$nextTick() + expect(wrapper.find('button.dropdown-toggle').text()).toBe('Français - fr') + }) + }) + + describe('language "nl" in store', () => { + it('shows Dutch as language', async () => { + wrapper.vm.$store.state.language = 'nl' + wrapper.vm.setCurrentLanguage() + await wrapper.vm.$nextTick() + expect(wrapper.find('button.dropdown-toggle').text()).toBe('Holandés - nl') + }) + }) + describe('dropdown menu', () => { it('has English and German as languages to choose', () => { - expect(wrapper.findAll('li')).toHaveLength(2) + expect(wrapper.findAll('li')).toHaveLength(5) }) it('has English as first language to choose', () => { @@ -113,6 +173,18 @@ describe('LanguageSwitch', () => { it('has German as second language to choose', () => { expect(wrapper.findAll('li').at(1).text()).toBe('Deutsch') }) + + it('has Español as second language to choose', () => { + expect(wrapper.findAll('li').at(2).text()).toBe('Español') + }) + + it('has French as second language to choose', () => { + expect(wrapper.findAll('li').at(3).text()).toBe('Français') + }) + + it('has Dutch as second language to choose', () => { + expect(wrapper.findAll('li').at(4).text()).toBe('Holandés') + }) }) }) @@ -138,6 +210,39 @@ describe('LanguageSwitch', () => { }), ) }) + + it("with locale 'es'", () => { + wrapper.findAll('li').at(2).find('a').trigger('click') + expect(updateUserInfosMutationMock).toBeCalledWith( + expect.objectContaining({ + variables: { + locale: 'es', + }, + }), + ) + }) + + it("with locale 'fr'", () => { + wrapper.findAll('li').at(3).find('a').trigger('click') + expect(updateUserInfosMutationMock).toBeCalledWith( + expect.objectContaining({ + variables: { + locale: 'fr', + }, + }), + ) + }) + + it("with locale 'nl'", () => { + wrapper.findAll('li').at(4).find('a').trigger('click') + expect(updateUserInfosMutationMock).toBeCalledWith( + expect.objectContaining({ + variables: { + locale: 'nl', + }, + }), + ) + }) }) }) }) diff --git a/frontend/src/components/LanguageSwitch2.spec.js b/frontend/src/components/LanguageSwitch2.spec.js index 600e2513e..080cc9d42 100644 --- a/frontend/src/components/LanguageSwitch2.spec.js +++ b/frontend/src/components/LanguageSwitch2.spec.js @@ -66,10 +66,37 @@ describe('LanguageSwitch', () => { expect(wrapper.findAll('span.locales').at(1).text()).toBe('Deutsch') }) }) - describe('navigator language is "es-ES" (not supported)', () => { + describe('navigator language is "es-ES"', () => { + const languageGetter = jest.spyOn(navigator, 'language', 'get') + it('shows Español as language ', async () => { + languageGetter.mockReturnValue('es-ES') + wrapper.vm.setCurrentLanguage() + await wrapper.vm.$nextTick() + expect(wrapper.findAll('span.locales').at(2).text()).toBe('Español') + }) + }) + describe('navigator language is "fr-FR"', () => { + const languageGetter = jest.spyOn(navigator, 'language', 'get') + it('shows French as language ', async () => { + languageGetter.mockReturnValue('fr-FR') + wrapper.vm.setCurrentLanguage() + await wrapper.vm.$nextTick() + expect(wrapper.findAll('span.locales').at(3).text()).toBe('Français') + }) + }) + describe('navigator language is "nl-NL"', () => { + const languageGetter = jest.spyOn(navigator, 'language', 'get') + it('shows Dutch as language ', async () => { + languageGetter.mockReturnValue('nl-NL') + wrapper.vm.setCurrentLanguage() + await wrapper.vm.$nextTick() + expect(wrapper.findAll('span.locales').at(4).text()).toBe('Holandés') + }) + }) + describe('navigator language is "it-IT" (not supported)', () => { const languageGetter = jest.spyOn(navigator, 'language', 'get') it('shows English as language ', async () => { - languageGetter.mockReturnValue('es-ES') + languageGetter.mockReturnValue('it-IT') wrapper.vm.setCurrentLanguage() await wrapper.vm.$nextTick() expect(wrapper.findAll('span.locales').at(0).text()).toBe('English') @@ -90,44 +117,62 @@ describe('LanguageSwitch', () => { wrapper.vm.$store.state.language = 'de' wrapper.vm.setCurrentLanguage() await wrapper.vm.$nextTick() - expect(wrapper.findAll('span.locales').at(1).text()).toBe('Deutsch') + expect(wrapper.findAll('span.locales').at(1).text()).toBe('English') + }) + }) + describe('language "es" in store', () => { + it('shows Español as language', async () => { + wrapper.vm.$store.state.language = 'es' + wrapper.vm.setCurrentLanguage() + await wrapper.vm.$nextTick() + expect(wrapper.findAll('span.locales').at(2).text()).toBe('Deutsch') + }) + }) + describe('language "fr" in store', () => { + it('shows French as language', async () => { + wrapper.vm.$store.state.language = 'fr' + wrapper.vm.setCurrentLanguage() + await wrapper.vm.$nextTick() + expect(wrapper.findAll('span.locales').at(3).text()).toBe('Español') + }) + }) + describe('language "nl" in store', () => { + it('shows Dutch as language', async () => { + wrapper.vm.$store.state.language = 'nl' + wrapper.vm.setCurrentLanguage() + await wrapper.vm.$nextTick() + expect(wrapper.findAll('span.locales').at(4).text()).toBe('Français') }) }) describe('language menu', () => { - it('has English and German as languages to choose', () => { - expect(wrapper.findAll('span.locales')).toHaveLength(2) + it('has English, German and Español as languages to choose', () => { + expect(wrapper.findAll('span.locales')).toHaveLength(5) }) it('has English as first language to choose', () => { - expect(wrapper.findAll('span.locales').at(0).text()).toBe('English') + expect(wrapper.findAll('span.locales').at(0).text()).toBe('Holandés') }) it('has German as second language to choose', () => { - expect(wrapper.findAll('span.locales').at(1).text()).toBe('Deutsch') + expect(wrapper.findAll('span.locales').at(1).text()).toBe('English') + }) + it('has Español as third language to choose', () => { + expect(wrapper.findAll('span.locales').at(2).text()).toBe('Deutsch') + }) + it('has French as third language to choose', () => { + expect(wrapper.findAll('span.locales').at(3).text()).toBe('Español') + }) + it('has Dutch as third language to choose', () => { + expect(wrapper.findAll('span.locales').at(4).text()).toBe('Français') }) }) }) describe('calls the API', () => { it("with locale 'de'", () => { - wrapper.findAll('span.locales').at(1).trigger('click') + wrapper.findAll('span.locales').at(2).trigger('click') expect(updateUserInfosMutationMock).toBeCalledWith( - expect.objectContaining({ - variables: { - locale: 'de', - }, - }), + expect.objectContaining({ variables: { locale: 'de' } }), ) }) - - // it("with locale 'en'", () => { - // wrapper.findAll('span.locales').at(0).trigger('click') - // expect(updateUserInfosMutationMock).toBeCalledWith( - // expect.objectContaining({ - // variables: { - // locale: 'en', - // }, - // }), - // ) - // }) }) }) }) diff --git a/frontend/src/components/LanguageSwitch2.vue b/frontend/src/components/LanguageSwitch2.vue index e88050047..786e43190 100644 --- a/frontend/src/components/LanguageSwitch2.vue +++ b/frontend/src/components/LanguageSwitch2.vue @@ -1,15 +1,37 @@ @@ -68,6 +90,17 @@ export default { this.currentLanguage = object }, }, + computed: { + indexOfSelectedLocale() { + return this.locales.findIndex((element) => element.code === this.$store.state.language) + }, + indexOfSecondLastLocale() { + return this.locales.length - 2 + }, + indexOfLastLocale() { + return this.locales.length - 1 + }, + }, created() { this.setCurrentLanguage() }, diff --git a/frontend/src/components/LanguageSwitchSelect.vue b/frontend/src/components/LanguageSwitchSelect.vue index 545cef4e9..109c020ff 100644 --- a/frontend/src/components/LanguageSwitchSelect.vue +++ b/frontend/src/components/LanguageSwitchSelect.vue @@ -16,6 +16,9 @@ export default { options: [ { value: 'de', text: this.$t('settings.language.de') }, { value: 'en', text: this.$t('settings.language.en') }, + { value: 'es', text: this.$t('settings.language.es') }, + { value: 'fr', text: this.$t('settings.language.fr') }, + { value: 'nl', text: this.$t('settings.language.nl') }, ], } }, diff --git a/frontend/src/components/Menu/Navbar.spec.js b/frontend/src/components/Menu/Navbar.spec.js index ebf9abba0..1e08ad9dc 100644 --- a/frontend/src/components/Menu/Navbar.spec.js +++ b/frontend/src/components/Menu/Navbar.spec.js @@ -17,7 +17,7 @@ const mocks = { $t: jest.fn((t) => t), $store: { state: { - hasElopage: false, + hasElopage: true, isAdmin: true, }, }, @@ -39,49 +39,85 @@ describe('Navbar', () => { expect(wrapper.find('div.component-navbar').exists()).toBeTruthy() }) - describe('navigation Navbar', () => { + describe('navigation Navbar (general elements)', () => { it('has .navbar-brand in the navbar', () => { expect(wrapper.find('.navbar-brand').exists()).toBeTruthy() }) + it('has b-navbar-toggle in the navbar', () => { expect(wrapper.find('.navbar-toggler').exists()).toBeTruthy() }) - it('has ten b-nav-item in the navbar', () => { - expect(wrapper.findAll('.nav-item')).toHaveLength(10) + + it('has thirteen b-nav-item in the navbar', () => { + expect(wrapper.findAll('.nav-item')).toHaveLength(13) }) - it('has first nav-item "amount GDD" in navbar', () => { + it('has nav-item "amount GDD" in navbar', () => { expect(wrapper.findAll('.nav-item').at(1).text()).toEqual('1234 GDD') }) - it('has first nav-item "navigation.overview" in navbar', () => { + it('has nav-item "navigation.overview" in navbar', () => { expect(wrapper.findAll('.nav-item').at(3).text()).toEqual('navigation.overview') }) - it('has first nav-item "navigation.send" in navbar', () => { + + it('has nav-item "navigation.send" in navbar', () => { expect(wrapper.findAll('.nav-item').at(4).text()).toEqual('navigation.send') }) - it('has first nav-item "navigation.transactions" in navbar', () => { + + it('has nav-item "navigation.transactions" in navbar', () => { expect(wrapper.findAll('.nav-item').at(5).text()).toEqual('navigation.transactions') }) - it('has first nav-item "navigation.profile" in navbar', () => { - expect(wrapper.findAll('.nav-item').at(6).text()).toEqual('navigation.profile') + + it('has nav-item "gdt.gdt" in navbar', () => { + expect(wrapper.findAll('.nav-item').at(6).text()).toEqual('gdt.gdt') }) + it('has nav-item "navigation.community" in navbar', () => { + expect(wrapper.findAll('.nav-item').at(7).text()).toEqual('navigation.community') + }) + + it('has nav-item "navigation.profile" in navbar', () => { + expect(wrapper.findAll('.nav-item').at(8).text()).toEqual('navigation.profile') + }) + + it('has nav-item "navigation.info" in navbar', () => { + expect(wrapper.findAll('.nav-item').at(9).text()).toEqual('navigation.info') + }) + }) + + describe('navigation Navbar (user has an elopage account)', () => { it('has a link to the members area', () => { - expect(wrapper.findAll('.nav-item').at(7).text()).toContain('navigation.members_area') - expect(wrapper.findAll('.nav-item').at(7).find('a').attributes('href')).toBe( + expect(wrapper.findAll('.nav-item').at(10).text()).toContain('navigation.members_area') + expect(wrapper.findAll('.nav-item').at(10).find('a').attributes('href')).toBe( 'https://elopage.com', ) }) - it('has first nav-item "navigation.admin_area" in navbar', () => { - expect(wrapper.findAll('.nav-item').at(8).text()).toEqual('navigation.admin_area') + it('has nav-item "navigation.admin_area" in navbar', () => { + expect(wrapper.findAll('.nav-item').at(11).text()).toEqual('navigation.admin_area') }) - it('has first nav-item "navigation.logout" in navbar', () => { - expect(wrapper.findAll('.nav-item').at(9).text()).toEqual('navigation.logout') + + it('has nav-item "navigation.logout" in navbar', () => { + expect(wrapper.findAll('.nav-item').at(12).text()).toEqual('navigation.logout') + }) + }) + + describe('navigation Navbar (user has no elopage account)', () => { + beforeAll(() => { + mocks.$store.state.hasElopage = false + wrapper = Wrapper() + }) + + it('has nav-item "navigation.admin_area" in navbar', () => { + expect(wrapper.findAll('.nav-item').at(10).text()).toEqual('navigation.admin_area') + }) + + it('has nav-item "navigation.logout" in navbar', () => { + expect(wrapper.findAll('.nav-item').at(11).text()).toEqual('navigation.logout') }) }) }) + describe('check watch visible true', () => { beforeEach(async () => { await wrapper.setProps({ visible: true }) diff --git a/frontend/src/components/Menu/Navbar.vue b/frontend/src/components/Menu/Navbar.vue index 2f26f381e..d1fe3af9d 100644 --- a/frontend/src/components/Menu/Navbar.vue +++ b/frontend/src/components/Menu/Navbar.vue @@ -52,17 +52,26 @@ {{ $t('navigation.transactions') }} + + + {{ $t('gdt.gdt') }} + + + + {{ $t('navigation.community') }} + {{ $t('navigation.profile') }} + + + {{ $t('navigation.info') }} +
- + {{ $t('navigation.members_area') }} - - {{ $t('math.exclaim') }} - diff --git a/frontend/src/components/Menu/Sidebar.spec.js b/frontend/src/components/Menu/Sidebar.spec.js index fec7945c8..612649762 100644 --- a/frontend/src/components/Menu/Sidebar.spec.js +++ b/frontend/src/components/Menu/Sidebar.spec.js @@ -27,38 +27,86 @@ describe('Sidebar', () => { beforeEach(() => { wrapper = Wrapper() }) + it('renders the component', () => { expect(wrapper.find('div#component-sidebar').exists()).toBeTruthy() }) describe('navigation Navbar', () => { - it('has seven b-nav-item in the navbar', () => { - expect(wrapper.findAll('.nav-item')).toHaveLength(7) + it('has ten b-nav-item in the navbar', () => { + expect(wrapper.findAll('.nav-item')).toHaveLength(10) }) - it('has first nav-item "navigation.overview" in navbar', () => { - expect(wrapper.findAll('.nav-item').at(0).text()).toEqual('navigation.overview') + describe('navigation Navbar (general elements)', () => { + it('has nav-item "navigation.overview" in navbar', () => { + expect(wrapper.findAll('.nav-item').at(0).text()).toEqual('navigation.overview') + }) + + it('has nav-item "navigation.send" in navbar', () => { + expect(wrapper.findAll('.nav-item').at(1).text()).toEqual('navigation.send') + }) + + it('has nav-item "gdt.gdt" in navbar', () => { + expect(wrapper.findAll('.nav-item').at(3).text()).toEqual('gdt.gdt') + }) + + it('has nav-item "navigation.community" in navbar', () => { + expect(wrapper.findAll('.nav-item').at(4).text()).toContain('navigation.community') + }) + + it('has nav-item "navigation.profile" in navbar', () => { + expect(wrapper.findAll('.nav-item').at(5).text()).toEqual('navigation.profile') + }) + + it('has nav-item "navigation.info" in navbar', () => { + expect(wrapper.findAll('.nav-item').at(6).text()).toEqual('navigation.info') + }) }) - it('has first nav-item "navigation.send" in navbar', () => { - expect(wrapper.findAll('.nav-item').at(1).text()).toEqual('navigation.send') + describe('navigation Navbar (user has an elopage account)', () => { + it('has ten b-nav-item in the navbar', () => { + expect(wrapper.findAll('.nav-item')).toHaveLength(10) + }) + + it('has a link to the members area', () => { + expect(wrapper.findAll('.nav-item').at(7).text()).toEqual('navigation.members_area') + expect(wrapper.findAll('.nav-item').at(7).find('a').attributes('href')).toBe('#') + }) + + it('has nav-item "navigation.admin_area" in navbar', () => { + expect(wrapper.findAll('.nav-item').at(8).text()).toEqual('navigation.admin_area') + }) + + it('has nav-item "navigation.logout" in navbar', () => { + expect(wrapper.findAll('.nav-item').at(9).text()).toEqual('navigation.logout') + }) }) - it('has first nav-item "navigation.transactions" in navbar', () => { - expect(wrapper.findAll('.nav-item').at(2).text()).toEqual('navigation.transactions') + it('has nav-item "navigation.admin_area" in navbar', () => { + expect(wrapper.findAll('.nav-item').at(8).text()).toEqual('navigation.admin_area') }) - it('has first nav-item "navigation.profile" in navbar', () => { - expect(wrapper.findAll('.nav-item').at(3).text()).toEqual('navigation.profile') + + it('has nav-item "navigation.logout" in navbar', () => { + expect(wrapper.findAll('.nav-item').at(9).text()).toEqual('navigation.logout') }) - it('has a link to the members area', () => { - expect(wrapper.findAll('.nav-item').at(4).text()).toContain('navigation.members_area') - expect(wrapper.findAll('.nav-item').at(4).find('a').attributes('href')).toBe('#') + }) + + describe('navigation Navbar (user has no elopage account)', () => { + beforeAll(() => { + mocks.$store.state.hasElopage = false + wrapper = Wrapper() }) - it('has first nav-item "navigation.admin_area" in navbar', () => { - expect(wrapper.findAll('.nav-item').at(5).text()).toEqual('navigation.admin_area') + + it('has nine b-nav-item in the navbar', () => { + expect(wrapper.findAll('.nav-item')).toHaveLength(9) }) - it('has first nav-item "navigation.logout" in navbar', () => { - expect(wrapper.findAll('.nav-item').at(6).text()).toEqual('navigation.logout') + + it('has nav-item "navigation.admin_area" in navbar', () => { + expect(wrapper.findAll('.nav-item').at(7).text()).toEqual('navigation.admin_area') + }) + + it('has nav-item "navigation.logout" in navbar', () => { + expect(wrapper.findAll('.nav-item').at(8).text()).toEqual('navigation.logout') }) }) }) diff --git a/frontend/src/components/Menu/Sidebar.vue b/frontend/src/components/Menu/Sidebar.vue index 028b7aca6..4b38851b2 100644 --- a/frontend/src/components/Menu/Sidebar.vue +++ b/frontend/src/components/Menu/Sidebar.vue @@ -16,19 +16,33 @@ {{ $t('navigation.transactions') }} + + + {{ $t('gdt.gdt') }} + + + + {{ $t('navigation.community') }} + {{ $t('navigation.profile') }} + + + {{ $t('navigation.info') }} +
- + {{ $t('navigation.members_area') }} - - {{ $t('math.exclaim') }} - diff --git a/frontend/src/components/TransactionLinks/TransactionLink.spec.js b/frontend/src/components/TransactionLinks/TransactionLink.spec.js index d06f0f726..798223f60 100644 --- a/frontend/src/components/TransactionLinks/TransactionLink.spec.js +++ b/frontend/src/components/TransactionLinks/TransactionLink.spec.js @@ -105,7 +105,8 @@ describe('TransactionLink', () => { 'http://localhost/redeem/c00000000c000000c0000\n' + 'Testy transaction-link.send_you 75 Gradido.\n' + '"Katzenauge, Eulenschrei, was verschwunden komm herbei!"\n' + - 'gdd_per_link.credit-your-gradido gdd_per_link.validUntilDate', + 'gdd_per_link.credit-your-gradido gdd_per_link.validUntilDate\n' + + 'gdd_per_link.link-hint', ) }) it('toasts success message', () => { diff --git a/frontend/src/components/TransactionLinks/TransactionLink.vue b/frontend/src/components/TransactionLinks/TransactionLink.vue index 5618c8696..76f705e35 100644 --- a/frontend/src/components/TransactionLinks/TransactionLink.vue +++ b/frontend/src/components/TransactionLinks/TransactionLink.vue @@ -18,17 +18,17 @@ - + - {{ $t('gdd_per_link.copy') }} + {{ $t('gdd_per_link.copy-link') }} - {{ $t('gdd_per_link.copy-with-text') }} + {{ $t('gdd_per_link.copy-link-with-text') }} { - this.toastSuccess(this.$t('gdd_per_link.link-copied')) - }) - .catch(() => { - this.$bvModal.show('modalPopoverCopyError' + this.id) - this.toastError(this.$t('gdd_per_link.not-copied')) - }) - }, - copyLinkWithText() { - navigator.clipboard - .writeText( - `${this.link} -${this.$store.state.firstName} ${this.$t('transaction-link.send_you')} ${this.amount} Gradido. -"${this.memo}" -${this.$t('gdd_per_link.credit-your-gradido')} ${this.$t('gdd_per_link.validUntilDate', { - date: this.$d(new Date(this.validUntil), 'short'), - })}`, - ) - .then(() => { - this.toastSuccess(this.$t('gdd_per_link.link-and-text-copied')) - }) - .catch(() => { - this.$bvModal.show('modalPopoverCopyError' + this.id) - this.toastError(this.$t('gdd_per_link.not-copied')) - }) - }, deleteLink() { this.$bvModal.msgBoxConfirm(this.$t('gdd_per_link.delete-the-link')).then(async (value) => { if (value) diff --git a/frontend/src/graphql/mutations.js b/frontend/src/graphql/mutations.js index 9b035cba6..959bdefc3 100644 --- a/frontend/src/graphql/mutations.js +++ b/frontend/src/graphql/mutations.js @@ -74,6 +74,9 @@ export const createTransactionLink = gql` mutation($amount: Decimal!, $memo: String!) { createTransactionLink(amount: $amount, memo: $memo) { link + amount + memo + validUntil } } ` @@ -89,3 +92,33 @@ export const redeemTransactionLink = gql` redeemTransactionLink(code: $code) } ` + +export const createContribution = gql` + mutation($creationDate: String!, $memo: String!, $amount: Decimal!) { + createContribution(creationDate: $creationDate, memo: $memo, amount: $amount) { + amount + memo + } + } +` + +export const updateContribution = gql` + mutation($contributionId: Int!, $amount: Decimal!, $memo: String!, $creationDate: String!) { + updateContribution( + contributionId: $contributionId + amount: $amount + memo: $memo + creationDate: $creationDate + ) { + id + amount + memo + } + } +` + +export const deleteContribution = gql` + mutation($id: Int!) { + deleteContribution(id: $id) + } +` diff --git a/frontend/src/graphql/queries.js b/frontend/src/graphql/queries.js index adcd653a4..03299dd49 100644 --- a/frontend/src/graphql/queries.js +++ b/frontend/src/graphql/queries.js @@ -13,6 +13,7 @@ export const login = gql` hasElopage publisherId isAdmin + creation } } ` @@ -30,6 +31,7 @@ export const verifyLogin = gql` hasElopage publisherId isAdmin + creation } } ` @@ -160,3 +162,96 @@ export const listTransactionLinks = gql` } } ` + +export const listContributionLinks = gql` + query($currentPage: Int = 1, $pageSize: Int = 25, $order: Order = DESC) { + listContributionLinks(currentPage: $currentPage, pageSize: $pageSize, order: $order) { + links { + id + amount + name + memo + createdAt + validFrom + validTo + maxAmountPerMonth + cycle + maxPerCycle + } + count + } + } +` + +export const listContributions = gql` + query( + $currentPage: Int = 1 + $pageSize: Int = 25 + $order: Order = DESC + $filterConfirmed: Boolean = false + ) { + listContributions( + currentPage: $currentPage + pageSize: $pageSize + order: $order + filterConfirmed: $filterConfirmed + ) { + contributionCount + contributionList { + id + amount + memo + createdAt + contributionDate + confirmedAt + confirmedBy + deletedAt + } + } + } +` + +export const listAllContributions = gql` + query($currentPage: Int = 1, $pageSize: Int = 25, $order: Order = DESC) { + listAllContributions(currentPage: $currentPage, pageSize: $pageSize, order: $order) { + contributionCount + contributionList { + id + firstName + lastName + amount + memo + createdAt + contributionDate + confirmedAt + confirmedBy + } + } + } +` + +export const communityStatistics = gql` + query { + communityStatistics { + totalUsers + activeUsers + deletedUsers + totalGradidoCreated + totalGradidoDecayed + totalGradidoAvailable + totalGradidoUnbookedDecayed + } + } +` + +export const searchAdminUsers = gql` + query { + searchAdminUsers { + userCount + userList { + firstName + lastName + } + } + } +` diff --git a/frontend/src/i18n.js b/frontend/src/i18n.js index f4f969008..243c35a24 100644 --- a/frontend/src/i18n.js +++ b/frontend/src/i18n.js @@ -1,9 +1,6 @@ import Vue from 'vue' import VueI18n from 'vue-i18n' -import en from 'vee-validate/dist/locale/en' -import de from 'vee-validate/dist/locale/de' - Vue.use(VueI18n) function loadLocaleMessages() { @@ -13,18 +10,9 @@ function loadLocaleMessages() { const matched = key.match(/([A-Za-z0-9-_]+)\./i) if (matched && matched.length > 1) { const locale = matched[1] - messages[locale] = locales(key) - if (locale === 'de') { - messages[locale] = { - validations: de, - ...messages[locale], - } - } - if (locale === 'en') { - messages[locale] = { - validations: en, - ...messages[locale], - } + messages[locale] = { + validations: require(`vee-validate/dist/locale/${locale}`), + ...locales(key), } } }) @@ -58,6 +46,45 @@ const numberFormats = { useGrouping: false, }, }, + es: { + decimal: { + style: 'decimal', + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }, + ungroupedDecimal: { + style: 'decimal', + minimumFractionDigits: 2, + maximumFractionDigits: 2, + useGrouping: false, + }, + }, + fr: { + decimal: { + style: 'decimal', + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }, + ungroupedDecimal: { + style: 'decimal', + minimumFractionDigits: 2, + maximumFractionDigits: 2, + useGrouping: false, + }, + }, + nl: { + decimal: { + style: 'decimal', + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }, + ungroupedDecimal: { + style: 'decimal', + minimumFractionDigits: 2, + maximumFractionDigits: 2, + useGrouping: false, + }, + }, } const dateTimeFormats = { @@ -75,6 +102,19 @@ const dateTimeFormats = { hour: 'numeric', minute: 'numeric', }, + monthShort: { + month: 'short', + }, + month: { + month: 'long', + }, + year: { + year: 'numeric', + }, + monthAndYear: { + month: 'long', + year: 'numeric', + }, }, de: { short: { @@ -90,6 +130,103 @@ const dateTimeFormats = { hour: 'numeric', minute: 'numeric', }, + monthShort: { + month: 'short', + }, + month: { + month: 'long', + }, + year: { + year: 'numeric', + }, + monthAndYear: { + month: 'long', + year: 'numeric', + }, + }, + es: { + short: { + day: 'numeric', + month: 'numeric', + year: 'numeric', + }, + long: { + day: 'numeric', + month: 'long', + year: 'numeric', + weekday: 'long', + hour: 'numeric', + minute: 'numeric', + }, + monthShort: { + month: 'short', + }, + month: { + month: 'long', + }, + year: { + year: 'numeric', + }, + monthAndYear: { + month: 'long', + year: 'numeric', + }, + }, + fr: { + short: { + day: 'numeric', + month: 'numeric', + year: 'numeric', + }, + long: { + day: 'numeric', + month: 'long', + year: 'numeric', + weekday: 'long', + hour: 'numeric', + minute: 'numeric', + }, + monthShort: { + month: 'short', + }, + month: { + month: 'long', + }, + year: { + year: 'numeric', + }, + monthAndYear: { + month: 'long', + year: 'numeric', + }, + }, + nl: { + short: { + day: 'numeric', + month: 'numeric', + year: 'numeric', + }, + long: { + day: 'numeric', + month: 'long', + year: 'numeric', + weekday: 'long', + hour: 'numeric', + minute: 'numeric', + }, + monthShort: { + month: 'short', + }, + month: { + month: 'long', + }, + year: { + year: 'numeric', + }, + monthAndYear: { + month: 'long', + year: 'numeric', + }, }, } diff --git a/frontend/src/locales/de.json b/frontend/src/locales/de.json index 378dcc919..dfd7c8335 100644 --- a/frontend/src/locales/de.json +++ b/frontend/src/locales/de.json @@ -26,11 +26,46 @@ "community": "Gemeinschaft", "continue-to-registration": "Weiter zur Registrierung", "current-community": "Aktuelle Gemeinschaft", + "members": "Mitglieder", + "moderators": "Moderatoren", + "myContributions": "Meine Beiträge zum Gemeinwohl", + "openContributionLinks": "öffentliche Beitrags-Linkliste", + "openContributionLinkText": "Folgende {count} automatische Schöpfungen werden zur Zeit durch die Gemeinschaft „{name}“ bereitgestellt.", "other-communities": "Weitere Gemeinschaften", + "statistic": "Statistik", + "submitContribution": "Beitrag einreichen", "switch-to-this-community": "zu dieser Gemeinschaft wechseln" }, + "contribution": { + "activity": "Tätigkeit", + "alert": { + "communityNoteList": "Hier findest du alle eingereichten und bestätigten Beiträge von allen Mitgliedern aus dieser Gemeinschaft.", + "confirm": "bestätigt", + "myContributionNoteList": "Eingereichte Beiträge, die noch nicht bestätigt wurden, kannst du jederzeit bearbeiten oder löschen.", + "myContributionNoteSupport": "Es wird bald an dieser Stelle die Möglichkeit geben das ein Dialog zwischen Moderatoren und dir stattfinden kann. Solltest du jetzt Probleme haben bitte nimm Kontakt mit dem Support auf.", + "pending": "Eingereicht und wartet auf Bestätigung", + "rejected": "abgelehnt" + }, + "date": "Beitrag für:", + "delete": "Beitrag löschen! Bist du sicher?", + "deleted": "Der Beitrag wurde gelöscht! Wird aber sichtbar bleiben.", + "formText": { + "bringYourTalentsTo": "Bring dich mit deinen Talenten in die Gemeinschaft ein! Dein freiwilliges Engagement honorieren wir mit 20 GDD pro Stunde bis maximal 1.000 GDD im Monat.", + "describeYourCommunity": "Beschreibe deine Gemeinwohl-Tätigkeit mit Angabe der Stunden und trage einen Betrag von 20 GDD pro Stunde ein! Nach Bestätigung durch einen Moderator wird der Betrag deinem Konto gutgeschrieben.", + "maxGDDforMonth": "Du kannst für den ausgewählten Monat nur noch maximal {amount} GDD einreichen.", + "openAmountForMonth": "Für {monthAndYear} kannst du noch {creation} GDD einreichen.", + "yourContribution": "Dein Beitrag zum Gemeinwohl" + }, + "noDateSelected": "Wähle irgendein Datum im Monat", + "selectDate": "Wann war dein Beitrag?", + "submit": "Einreichen", + "submitted": "Der Beitrag wurde eingereicht.", + "updated": "Der Beitrag wurde geändert.", + "yourActivity": "Bitte trage eine Tätigkeit ein!" + }, "contribution-link": { - "thanksYouWith": "dankt dir mit" + "thanksYouWith": "dankt dir mit", + "unique": "(einmalig)" }, "decay": { "before_startblock_transaction": "Diese Transaktion beinhaltet keine Vergänglichkeit.", @@ -78,17 +113,18 @@ "amount": "Betrag", "at": "am", "cancel": "Abbrechen", + "change": "Ändern", "check_now": "Jetzt prüfen", "close": "Schließen", "current_balance": "Aktueller Kontostand", "date": "Datum", "description": "Beschreibung", - "edit": "Bearbeiten", "email": "E-Mail", "firstname": "Vorname", "from": "Von", "generate_now": "Jetzt generieren", "lastname": "Nachname", + "mandatoryField": "Pflichtfeld", "memo": "Nachricht", "message": "Nachricht", "new_balance": "Neuer Kontostand nach Bestätigung", @@ -123,8 +159,8 @@ "GDD": "GDD", "gdd_per_link": { "choose-amount": "Wähle einen Betrag aus, welchen du per Link versenden möchtest. Du kannst auch noch eine Nachricht eintragen. Beim Klick „Jetzt generieren“ wird ein Link erstellt, den du versenden kannst.", - "copy": "kopieren", - "copy-with-text": "Link und Text kopieren", + "copy-link": "Link kopieren", + "copy-link-with-text": "Link und Text kopieren", "created": "Der Link wurde erstellt!", "credit-your-gradido": "Damit die Gradido gutgeschrieben werden können, klicke auf den Link!", "decay-14-day": "Vergänglichkeit für 14 Tage", @@ -138,6 +174,7 @@ "link-copied": "Link wurde in die Zwischenablage kopiert. Du kannst ihn jetzt in eine E-Mail oder Nachricht einfügen.", "link-deleted": "Der Link wurde am {date} gelöscht.", "link-expired": "Der Link ist nicht mehr gültig. Die Gültigkeit ist am {date} abgelaufen.", + "link-hint": "Achtung: Jeder kann diesen Link einlösen. Gib ihn bitte nicht weiter!", "link-overview": "Linkübersicht", "links_count": "Aktive Links", "links_sum": "Offene Links und QR-Codes", @@ -176,8 +213,8 @@ "login": "Anmeldung", "math": { "aprox": "~", + "asterisk": "*", "equal": "=", - "exclaim": "!", "minus": "−", "pipe": "|" }, @@ -193,6 +230,8 @@ }, "navigation": { "admin_area": "Adminbereich", + "community": "Gemeinschaft", + "info": "Information", "logout": "Abmelden", "members_area": "Mitgliederbereich", "overview": "Übersicht", @@ -215,6 +254,9 @@ "changeLanguage": "Sprache ändern", "de": "Deutsch", "en": "English", + "es": "Español", + "fr": "Français", + "nl": "Dutch", "success": "Deine Sprache wurde erfolgreich geändert." }, "name": { @@ -266,11 +308,20 @@ "uppercase": "Großbuchstabe erforderlich." } }, + "statistic": { + "activeUsers": "Aktive Mitglieder", + "deletedUsers": "Gelöschte Mitglieder", + "totalGradidoAvailable": "GDD insgesamt im Umlauf", + "totalGradidoCreated": "GDD insgesamt geschöpft", + "totalGradidoDecayed": "GDD insgesamt verfallen", + "totalGradidoUnbookedDecayed": "Gesamter ungebuchter GDD Verfall" + }, "success": "Erfolg", "time": { "days": "Tage", "hours": "Stunden", "minutes": "Minuten", + "month": "Monat", "months": "Monate", "seconds": "Sekunden", "years": "Jahr" diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index 0bfde65ed..ff46cf95a 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -26,11 +26,46 @@ "community": "Community", "continue-to-registration": "Continue to registration", "current-community": "Current community", + "members": "Members", + "moderators": "Moderators", + "myContributions": "My contributions to the common good", + "openContributionLinks": "open Contribution links list", + "openContributionLinkText": "The following {count} automatic creations are currently provided by the \"{name}\" community.", "other-communities": "Other communities", + "statistic": "Statistics", + "submitContribution": "Submit contribution", "switch-to-this-community": "Switch to this community" }, + "contribution": { + "activity": "Activity", + "alert": { + "communityNoteList": "Here you will find all submitted and confirmed contributions from all members of this community.", + "confirm": "confirmed", + "myContributionNoteList": "You can edit or delete entries that have not yet been confirmed at any time.", + "myContributionNoteSupport": "Soon there will be the possibility for a dialogue between moderators and you. If you have any problems now, please contact the support.", + "pending": "Submitted and waiting for confirmation", + "rejected": "deleted" + }, + "date": "Contribution for:", + "delete": "Delete Contribution! Are you sure?", + "deleted": "The contribution has been deleted! But it will remain visible.", + "formText": { + "bringYourTalentsTo": "Bring your talents to the community! Your voluntary commitment will be rewarded with 20 GDD per hour up to a maximum of 1,000 GDD per month.", + "describeYourCommunity": "Describe your community service activity with hours and enter an amount of 20 GDD per hour! After confirmation by a moderator, the amount will be credited to your account.", + "maxGDDforMonth": "You can only submit a maximum of {amount} GDD for the selected month.", + "openAmountForMonth": "For {monthAndYear}, you can still submit {creation} GDD.", + "yourContribution": "Your contribution to the common good" + }, + "noDateSelected": "Choose any date in the month", + "selectDate": "When was your contribution?", + "submit": "Submit", + "submitted": "The contribution was submitted.", + "updated": "The contribution was changed.", + "yourActivity": "Please enter an activity!" + }, "contribution-link": { - "thanksYouWith": "thanks you with" + "thanksYouWith": "thanks you with", + "unique": "(unique)" }, "decay": { "before_startblock_transaction": "This transaction does not include decay.", @@ -78,17 +113,18 @@ "amount": "Amount", "at": "at", "cancel": "Cancel", + "change": "Change", "check_now": "Check now", "close": "Close", "current_balance": "Current Balance", "date": "Date", "description": "Description", - "edit": "Edit", "email": "Email", "firstname": "Firstname", "from": "from", "generate_now": "Generate now", "lastname": "Lastname", + "mandatoryField": "mandatory field", "memo": "Message", "message": "Message", "new_balance": "Account balance after confirmation", @@ -123,8 +159,8 @@ "GDD": "GDD", "gdd_per_link": { "choose-amount": "Select an amount that you would like to send via link. You can also enter a message. Click 'Generate now' to create a link that you can share.", - "copy": "copy", - "copy-with-text": "Copy link and text", + "copy-link": "Copy link", + "copy-link-with-text": "Copy link and text", "created": "Link was created!", "credit-your-gradido": "For the Gradido to be credited, click on the link!", "decay-14-day": "Decay for 14 days", @@ -138,6 +174,7 @@ "link-copied": "Link has been copied to the clipboard. You can now paste it into an email or message.", "link-deleted": "The link was deleted on {date}.", "link-expired": "The link is no longer valid. The validity expired on {date}.", + "link-hint": "Attention: Anyone can redeem this link. Please do not share it!", "link-overview": "Link overview", "links_count": "Active links", "links_sum": "Open links and QR codes", @@ -176,8 +213,8 @@ "login": "Login", "math": { "aprox": "~", + "asterisk": "*", "equal": "=", - "exclaim": "!", "minus": "−", "pipe": "|" }, @@ -193,6 +230,8 @@ }, "navigation": { "admin_area": "Admin Area", + "community": "Community", + "info": "Information", "logout": "Logout", "members_area": "Members area", "overview": "Overview", @@ -215,6 +254,9 @@ "changeLanguage": "Change language", "de": "Deutsch", "en": "English", + "es": "Español", + "fr": "Français", + "nl": "Holandés", "success": "Your language has been successfully updated." }, "name": { @@ -266,11 +308,20 @@ "uppercase": "One uppercase letter required." } }, + "statistic": { + "activeUsers": "Active members", + "deletedUsers": "Deleted members", + "totalGradidoAvailable": "Total GDD in circulation", + "totalGradidoCreated": "Total created GDD", + "totalGradidoDecayed": "Total GDD decay", + "totalGradidoUnbookedDecayed": "Total unbooked GDD decay" + }, "success": "Success", "time": { "days": "Days", "hours": "Hours", "minutes": "Minutes", + "month": "Month", "months": "Months", "seconds": "Seconds", "years": "Year" diff --git a/frontend/src/locales/es.json b/frontend/src/locales/es.json new file mode 100644 index 000000000..834c74af4 --- /dev/null +++ b/frontend/src/locales/es.json @@ -0,0 +1,342 @@ +{ + "100": "100%", + "1000thanks": "1000 Gracias, por estar con nosotros!", + "125": "125%", + "85": "85%", + "advanced-calculation": "Proyección", + "auth": { + "left": { + "dignity": "Dignidad", + "donation": "Donación", + "gratitude": "Gratitud", + "hasAccount": "Ya estas registrado?", + "hereLogin": "Regístrate aquí", + "learnMore": "Infórmate aquí …", + "oneDignity": "Damos los unos a los otros y agradecemos con Gradido.", + "oneDonation": "Eres un regalo para la comunidad. 1000 gracias por estar con nosotros.", + "oneGratitude": "Por los demás, por toda la humanidad, por la naturaleza." + }, + "navbar": { + "aboutGradido": "Sobre Gradido" + } + }, + "back": "Volver", + "community": { + "choose-another-community": "Escoger otra comunidad", + "community": "Comunidad", + "continue-to-registration": "Continuar con el registro", + "current-community": "Comunidad actual", + "members": "Miembros", + "moderators": "Moderadores", + "myContributions": "Mis contribuciones al bien común", + "openContributionLinks": "lista de enlaces de contribuciones públicas", + "openContributionLinkText": "La comunidad \"{name}\" proporciona actualmente las siguientes {count} creaciones automáticas.", + "other-communities": "Otras comunidades", + "statistic": "Estadísticas", + "submitContribution": "Aportar una contribución", + "switch-to-this-community": "cambiar a esta comunidad" + }, + "contribution": { + "activity": "Actividad", + "alert": { + "communityNoteList": "Aquí encontrarás todas las contribuciones enviadas y confirmadas de todos los miembros de esta comunidad.", + "confirm": "confirmado", + "myContributionNoteList": "Puedes editar o eliminar las contribuciones enviadas que aún no han sido confirmadas en cualquier momento.", + "myContributionNoteSupport": "Pronto existirá la posibilidad de que puedas dialogar con los moderadores. Si tienes algún problema ahora, ponte en contacto con el equipo de asistencia.", + "pending": "Enviado y a la espera de confirmación", + "rejected": "rechazado" + }, + "date": "Contribución para:", + "delete": "Eliminar la contribución. ¿Estás seguro?", + "deleted": "¡La contribución ha sido borrada! Pero seguirá siendo visible.", + "formText": { + "bringYourTalentsTo": "¡Contribuye a la comunidad con tus talentos! Premiamos tu compromiso voluntario con 20 GDD por hora hasta un máximo de 1.000 GDD al mes.", + "describeYourCommunity": "¡Describe tu contribución al bien-común con detalles de las horas e introduce una cantidad de 20 GDD por hora! Tras la confirmación de un moderador, el importe se abonará en tu cuenta.", + "maxGDDforMonth": "Sólo puede presentar un máximo de {amount} GDD para el mes seleccionado.", + "openAmountForMonth": "Para {monthAndYear} aún puedes presentar {creation} GDD.", + "yourContribution": "Tu contribución a la comunidad." + }, + "noDateSelected": "Elige cualquier fecha del mes.", + "selectDate": "¿Cuando fue tu contribución?", + "submit": "Enviar", + "submitted": "Tu contribución ha sido enviada.", + "updated": "La contribución se modificó.", + "yourActivity": "¡Por favor, introduce una actividad!" + }, + "contribution-link": { + "thanksYouWith": "te agradece con", + "unique": "(único)" + }, + "decay": { + "before_startblock_transaction": "Esta transacción no implica disminución en su valor.", + "calculation_decay": "Cálculo de la disminución gradual del valor", + "calculation_total": "Cálculo de la suma total", + "decay": "Disminución gradual del valor", + "decay_introduced": "La disminución gradual empezó el:", + "decay_since_last_transaction": "Disminución gradual", + "last_transaction": "Transacción anterior", + "past_time": "Tiempo transcurrido", + "Starting_block_decay": "Startblock disminución gradual", + "total": "Total", + "types": { + "creation": "Creado", + "noDecay": "sin disminución gradual", + "receive": "Recibido", + "send": "Enviado" + } + }, + "delete": "Eliminar", + "em-dash": "—", + "error": { + "email-already-sent": "Ya te hemos enviado un correo electrónico hace menos de 10 minutos.", + "empty-transactionlist": "Ha habido un error en la transmisión del número de sus transacciones.", + "error": "Error!", + "no-account": "Lamentablemente no hemos podido encontrar una cuenta (activada) con estos datos.", + "no-transactionlist": "Lamentablemente, hubo un error. No se ha transmitido ninguna transacción desde el servidor.", + "no-user": "No hay usuario con estas referencias.", + "session-expired": "La sesión se cerró por razones de seguridad.", + "unknown-error": "Error desconocido: " + }, + "followUs": "sigue nos:", + "footer": { + "app_version": "App versión {version}", + "copyright": { + "link": "Gradido-Akademie", + "year": "© {year}" + }, + "imprint": "Aviso legal", + "privacy_policy": "Protección de Datos", + "short_hash": "({shortHash})", + "whitepaper": "Whitepaper" + }, + "form": { + "amount": "Importe", + "at": "am", + "cancel": "Cancelar", + "change": "Cambiar", + "check_now": "Revisar", + "close": "Cerrar", + "current_balance": "Saldo de cuenta actual", + "date": "Fecha", + "description": "Descripción", + "email": "E-Mail", + "firstname": "Nombre", + "from": "De", + "generate_now": "crear ahora", + "lastname": "Apellido", + "mandatoryField": "campo obligatorio", + "memo": "Mensaje", + "message": "Noticia", + "new_balance": "Saldo de cuenta nuevo depués de confirmación", + "no_gdd_available": "No dispones de GDD para enviar.", + "password": "Contraseña", + "passwordRepeat": "Repetir contraseña", + "password_new": "contraseña nueva", + "password_new_repeat": "Repetir contraseña nueva", + "password_old": "contraseña antigua", + "recipient": "Destinatario", + "reset": "Restablecer", + "save": "Guardar", + "scann_code": "QR Code Scanner - Escanea el código QR de tu pareja", + "sender": "Remitente", + "send_check": "Confirma tu transacción. Por favor revisa toda la información nuevamente!", + "send_now": "Enviar ahora", + "send_transaction_error": "Desafortunadamente, la transacción no se pudo ejecutar!", + "send_transaction_success": "Su transacción fue ejecutada con éxito", + "sorry": "Disculpa", + "thx": "Gracias", + "time": "Tiempo", + "to": "hasta", + "to1": "para", + "validation": { + "gddSendAmount": "El campo {_field_} debe ser un número entre {min} y {max} con un máximo de dos decimales", + "is-not": "No es posible transferirte Gradidos a ti mismo", + "usernmae-regex": "El nombre de usuario debe comenzar con una letra seguida de al menos dos caracteres alfanuméricos.", + "usernmae-unique": "Este nombre de usuario ya está adjudicado." + }, + "your_amount": "Tu importe" + }, + "GDD": "GDD", + "gdd_per_link": { + "choose-amount": "Selecciona una cantidad que te gustaría enviar a través de un enlace. También puedes ingresar un mensaje. Cuando haces clic en 'Generar ahora', se crea un enlace que puedes enviar.", + "copy-link": "Copiar enlace", + "copy-link-with-text": "Copiar texto y enlace", + "created": "El enlace ha sido creado", + "credit-your-gradido": "Para que se te acrediten los Gradidos, haz clic en el enlace!", + "decay-14-day": "Disminución gradual por 14 días", + "delete-the-link": "Eliminar el enlace?", + "deleted": "El enlace ha sido eliminado!", + "expiredOn": "Vencido el:", + "has-account": "Ya tienes una cuenta Gradido?", + "header": "Transferir Gradidos por medio de un enlace", + "isFree": "Gradido es gratis en todo el mundo.", + "link-and-text-copied": "El enlace y su mensaje se han copiado en el portapapeles. Ahora puedes ponerlo en un correo electrónico o mensaje.", + "link-copied": "El enlace se ha copiado en el portapapeles. Ahora puedes pegarlo en un correo electrónico o mensaje.", + "link-deleted": "El enlace se eliminó el {date}.", + "link-expired": "El enlace ya no es válido. La validez expiró el {date}.", + "link-hint": "Atención: cualquiera puede canjear este enlace. Por favor, no lo transmitan.", + "link-overview": "Resumen de enlaces", + "links_count": "Enlaces activos", + "links_sum": "Enlaces abiertos y códigos QR", + "no-account": "Aún no tienes una cuenta de Gradido?", + "no-redeem": "No puedes canjear tu propio enlace!", + "not-copied": "¡Desafortunadamente, su dispositivo no permite copiar! Copie el enlace manualmente!", + "redeem": "Canjear", + "redeem-text": "¿Quieres canjear el importe ahora?", + "redeemed": "¡Canjeado con éxito! Tu cuenta ha sido acreditada con {n} GDD.", + "redeemed-at": "El enlace ya se canjeó el {date}.", + "redeemed-title": "canjeado", + "to-login": "iniciar sesión", + "to-register": "Registre una nueva cuenta.", + "validUntil": "Válido hasta", + "validUntilDate": "El enlace es válido hasta el {date} ." + }, + "gdt": { + "calculation": "Cálculo del Gradido Transform", + "contribution": "Importe", + "conversion": "Conversión", + "conversion-gdt-euro": "Conversión Euro / Gradido Transform (GDT)", + "credit": "Abono", + "factor": "Factor", + "formula": "Formula de cálculo", + "funding": "Las donaciones", + "gdt": "Gradido Transform", + "gdt-received": "Gradido Transform (GDT) recibido", + "no-transactions": "Aún no tienes un Gradido Transform (GDT).", + "not-reachable": "No es posible acceder al servidor GDT.", + "publisher": "Tu nuevo miembro referido ha pagado la cuota", + "raise": "Aumento", + "recruited-member": "Miembro invitado" + }, + "language": "Idioma", + "link-load": "recargar el último enlace |recargar los últimos {n} enlaces | descargar más {n} enlaces", + "login": "iniciar sesión", + "math": { + "aprox": "~", + "asterisk": "*", + "equal": "=", + "minus": "−", + "pipe": "|" + }, + "message": { + "activateEmail": "Tu cuenta aún no ha sido activada. Por favor revisa tu correo electrónico y haz clic en el enlace de activación o solicita uno nuevo enlace de activación a través de la página restablecer contraseña.", + "checkEmail": "Tu correo electrónico ha sido verificado con éxito. Puedes registrarte ahora.", + "email": "Te hemos enviado un correo electrónico.", + "errorTitle": "Atención!", + "register": "Ya estás registrado, por favor revisa tu correo electrónico y haz clic en el enlace de activación.", + "reset": "Tu contraseña ha sido cambiada.", + "title": "Gracias!", + "unsetPassword": "Tu contraseña aún no ha sido configurada. Por favor reinícialo." + }, + "navigation": { + "admin_area": "Área de administración", + "community": "Comunidad", + "info": "Información", + "logout": "Salir", + "members_area": "Área de afiliados", + "overview": "Resumen", + "profile": "Mi Perfil", + "send": "Enviar", + "support": "Soporte", + "transactions": "Transacciones" + }, + "qrCode": "Código QR", + "send_gdd": "Enviar GDD", + "send_per_link": "Enviar GDD mediante un enlace", + "session": { + "extend": "Permanecer en sesión iniciada", + "lightText": "Si no has realizado ninguna acción durante más de 10 minutos, se cerrará tu sesión por razones de seguridad.", + "logoutIn": "Cerrar sesión en ", + "warningText": "Aún estas?" + }, + "settings": { + "language": { + "changeLanguage": "Cambiar idioma", + "de": "Deutsch", + "en": "English", + "es": "Español", + "fr": "Francés", + "nl": "Holandés", + "success": "Tu idioma ha sido cambiado con éxito." + }, + "name": { + "change-name": "Cambiar nombre", + "change-success": "Tu nombre ha sido cambiado con éxito." + }, + "newsletter": { + "newsletter": "Informaciones por correo electrónico", + "newsletterFalse": "No recibirás informaciones por correo electrónico.", + "newsletterTrue": "Recibirás informaciones por correo electrónico." + }, + "password": { + "change-password": "Cambiar contraseña", + "forgot_pwd": "Olvide la contraseña?", + "resend_subtitle": "Su enlace de activación ha caducado. Puedes solicitar uno nuevo aquí.", + "reset": "Restablecer contraseña", + "reset-password": { + "text": "Ahora introduce una nueva contraseña con la que quieras acceder a tu cuenta de Gradido en el futuro.." + }, + "send_now": "Enviar", + "set": "Establecer contraseña", + "set-password": { + "text": "Ahora guarda tu nueva contraseña, que podrás utilizar para acceder a tu cuenta de Gradido en el futuro." + }, + "subtitle": "Si has olvidado tu contraseña, puedes restablecerla aquí." + } + }, + "signin": "Iniciar sesión", + "signup": "Registrarse", + "site": { + "forgotPassword": { + "heading": "Por favor, introduce la dirección de correo electrónico con la que estas registrado en Gradido." + }, + "login": { + "heading": "Inicia sesión con tus datos de acceso. Manténlos seguros en todo momento!" + }, + "resetPassword": { + "heading": "Por favor, introduce tu contraseña y repítela." + }, + "signup": { + "agree": "Acepto la Política de privacidad.", + "dont_match": "Las contraseñas no coinciden.", + "heading": "Regístrate introduciendo todos los datos completos y en los campos correctos.", + "lowercase": "Se requiere una letra minúscula.", + "minimum": "Al menos 8 caracteres.", + "no-whitespace": "Sin espacios ni tabulaciones.", + "one_number": "Se requiere un número.", + "special-char": "Caracteres especiales requeridos (por ejemplo, _ o &)", + "uppercase": "Letra mayúscula requerida." + } + }, + "statistic": { + "activeUsers": "miembros activos", + "deletedUsers": "miembros eliminados", + "totalGradidoAvailable": "GDD total en circulación", + "totalGradidoCreated": "GDD total creado", + "totalGradidoDecayed": "GDD total decaído", + "totalGradidoUnbookedDecayed": "GDD no contabilizado decaído" + }, + "success": "Lo lograste", + "time": { + "days": "Días", + "hours": "Horas", + "minutes": "Minutos", + "month": "Mes", + "months": "Meses", + "seconds": "Segundos", + "years": "Año" + }, + "transaction": { + "gdd-text": "Transacciones Gradido", + "gdt-text": "Transacciones GradidoTransform ", + "nullTransactions": "Todavía no tienes ninguna transacción en tu cuenta.", + "receiverDeleted": "La cuenta del destinatario ha sido eliminada.", + "receiverNotFound": "Destinatario no encontrado", + "show_all": "Ver todas las transacciones de {count}" + }, + "transaction-link": { + "send_you": "te envía" + }, + "via_link": "atraves de un enlace", + "welcome": "Bienvenido a la comunidad." +} diff --git a/frontend/src/locales/fr.json b/frontend/src/locales/fr.json new file mode 100644 index 000000000..e2e8747b5 --- /dev/null +++ b/frontend/src/locales/fr.json @@ -0,0 +1,342 @@ +{ + "100": "100%", + "1000thanks": "1000 mercis d'être avec nous!", + "125": "125%", + "85": "85%", + "advanced-calculation": "Calcul avancé", + "auth": { + "left": { + "dignity": "Dignité", + "donation": "Donation", + "gratitude": "Gratitude", + "hasAccount": "Avez-vous déjà un compte?", + "hereLogin": "Connectez-vous ici", + "learnMore": "Pour plus de détails …", + "oneDignity": "Nous nous donnons mutuellement et rendons grâce avec Gradido.", + "oneDonation": "Vous êtes précieux pour la communauté. 1000 mercis d’être parmi nous.", + "oneGratitude": "Les uns pour les autres, pour tout le monde, pour la nature." + }, + "navbar": { + "aboutGradido": "À propos de Gradido" + } + }, + "back": "Retour", + "community": { + "choose-another-community": "Choisissez une autre communauté", + "community": "Communauté", + "continue-to-registration": "Continuez l´inscription", + "current-community": "Communauté actuelle", + "members": "Membres", + "moderators": "Modérateurs", + "myContributions": "Mes contributions aux biens communs", + "openContributionLinks": "liste de liens de contribution publique", + "openContributionLinkText": "Les {count} créations automatiques suivantes sont actuellement fournies par la communauté \"{name}\".", + "other-communities": "Autres communautés", + "statistic": "Statistiques", + "submitContribution": "Donner une contribution", + "switch-to-this-community": "Passer à cette communauté" + }, + "contribution": { + "activity": "Activité", + "alert": { + "communityNoteList": "Vous trouverez ci-contre toutes les contributions versées et certifiées de tous les membres de cette communauté.", + "confirm": " Approuvé", + "myContributionNoteList": "À tout moment vous pouvez éditer ou supprimer les données qui n´ont pas été confirmées.", + "myContributionNoteSupport": "Vous aurez bientôt la possibilité de dialoguer avec un médiateur. Si vous rencontrez un problème maintenant, merci de contacter l´aide en ligne.", + "pending": "Inscription en attente de validation", + "rejected": "supprimé" + }, + "date": "Contribution pour:", + "delete": "Supprimer la contribution! Êtes-vous sûr?", + "deleted": "La contribution a été supprimée! Mais elle restera visible.", + "formText": { + "bringYourTalentsTo": "Apportez vos talents à la communauté! Votre participation bénévole sera récompensée de 20 GDD/heure jusqu´à un plafond de 1000 GDD/mois.", + "describeYourCommunity": "Décrivez votre activité/service à la communauté en mentionnant le nombre d´heures, et calculez le montant à raison de 20 GDD/heure! Après confirmation par l´un de nos collaborateurs, le montant sera crédité sur votre compte.", + "maxGDDforMonth": "Vous pouvez seulement déclarer un montant maximum de (montant) GDD pour le mois sélectionné.", + "openAmountForMonth": "Pour {monthAndYear}, vous pouvez encore déclarer {creation} GDD.", + "yourContribution": "Votre contribution au bien commun" + }, + "noDateSelected": "Choisissez n´importe quelle date du mois", + "selectDate": "Quand a été effectuée votre contribution?", + "submit": "Soumettre", + "submitted": "La contribution a été soumise.", + "updated": "La contribution a été modifiée.", + "yourActivity": "Veuillez saisir une activité!" + }, + "contribution-link": { + "thanksYouWith": "vous remercie avec", + "unique": "(unique)" + }, + "decay": { + "before_startblock_transaction": "Cette transaction n´est pas péremptoire.", + "calculation_decay": "Calcul de la décroissance", + "calculation_total": "Calcul du montant total", + "decay": "Décroissance", + "decay_introduced": "Décroissance engagée le:", + "decay_since_last_transaction": "Décroissance depuis la dernière transaction", + "last_transaction": "Dernière transaction:", + "past_time": "Le temps a expiré", + "Starting_block_decay": "Début de la décroissance", + "total": "Total", + "types": { + "creation": "Créé", + "noDecay": "Aucun déclin", + "receive": "Reçu", + "send": "Envoyé" + } + }, + "delete": "Supprimer", + "em-dash": "—", + "error": { + "email-already-sent": "Nous vous avons déjà envoyé un email il y a moins de 10 minutes.", + "empty-transactionlist": "Il y a eu une erreur lors de la transmission du numéro de votre transaction.", + "error": "Erreur!", + "no-account": "Malheureusement nous n´avons pas pu trouver de compte (actif) correspondant aux données transmises.", + "no-transactionlist": "Il y a malheureusement eu une erreur. Aucune transaction n´a été envoyée depuis l`serveur.", + "no-user": "Pas d`utilisateur pour cet identifiant.", + "session-expired": "La session a expiré pour des raisons de sécurité.", + "unknown-error": "Erreur inconnue: " + }, + "followUs": "Suivez-nous:", + "footer": { + "app_version": "Version de l'application {version}", + "copyright": { + "link": "Académie Gradido", + "year": "© {year}" + }, + "imprint": "Notification légale", + "privacy_policy": "Politique de confidentialité", + "short_hash": "({shortHash})", + "whitepaper": "Papier blanc" + }, + "form": { + "amount": "Montant", + "at": "à", + "cancel": "Annuler", + "change": "Changer", + "check_now": "Vérifier maintenant", + "close": "Fermer", + "current_balance": "Solde actuel", + "date": "Date", + "description": "Description", + "email": "Email", + "firstname": " Prénom", + "from": "de", + "generate_now": "Produire maintenant", + "lastname": "Nom", + "mandatoryField": "champ obligatoire", + "memo": "Note", + "message": "Message", + "new_balance": "Montant du solde après confirmation", + "no_gdd_available": "Vous n´avez pas de GDD à envoyer.", + "password": "Mot de passe", + "passwordRepeat": "Répétez le mot de passe", + "password_new": "Nouveau mot de passe", + "password_new_repeat": "Répétez le nouveau mot de passe", + "password_old": "Ancien mot de passe", + "recipient": "Destinataire", + "reset": "Réinitialiser", + "save": "Sauvegarder", + "scann_code": "QR Code Scanner - Scannez le QR code de votre partenaire", + "sender": "Expéditeur", + "send_check": "Confirmez la transaction. Veuillez revérifier toutes les données svp!", + "send_now": "Envoyez maintenant", + "send_transaction_error": "Malheureusement, la transaction n´a pas pu être effectuée!", + "send_transaction_success": "Votre transaction a été effectuée avec succès", + "sorry": "Désolé", + "thx": "Merci", + "time": "Heure", + "to": "à", + "to1": "à", + "validation": { + "gddSendAmount": "L´espace {_field_} doit comprendre un nombre entre {min} et {max} avec un maximum de deux chiffres après la virgule", + "is-not": "Vous ne pouvez pas vous envoyer de Gradido à vous-même", + "usernmae-regex": "Le nom d´utilisateur doit commencer par une lettre, suivi d´au moins deux caractères alphanumériques.", + "usernmae-unique": "Ce nom d´utilisateur est déjà pris." + }, + "your_amount": "Votre montant" + }, + "GDD": "GDD", + "gdd_per_link": { + "choose-amount": "Sélectionnez le montant que vous souhaitez envoyer via lien. Vous pouvez également joindre un message. Cliquez sur ‘créer maintenant’ pour établir un lien que vous pourrez partager.", + "copy-link": "Copier le lien", + "copy-link-with-text": "Copier le lien et le texte", + "created": "Le lien a été créé!", + "credit-your-gradido": "Pour l´accréditation du Gradido, cliquer sur le lien!", + "decay-14-day": "Perte sur 14 jours", + "delete-the-link": "Supprimer le lien?", + "deleted": "Le lien a été supprimé!", + "expiredOn": "A expiré le", + "has-account": "Vous avez déjà un compte Gradido?", + "header": "Envoyer des Gradidos via lien", + "isFree": "Gradido est gratuit mondialement.", + "link-and-text-copied": "Le lien et votre message ont été copiés dans le presse-papier. Vous pouvez maintenant le joindre à un email ou à un message..", + "link-copied": "Le lien a été copié dans le presse-papier. Vous pouvez désormais le coller dans votre email ou votre message.", + "link-deleted": "Le lien a été supprimé le on {date}.", + "link-expired": "Le lien n´est plus valide. Sa validité a expiré le {date}.", + "link-hint": "Attention : tout le monde peut utiliser ce lien. Ne le partage pas, s'il te plaît!", + "link-overview": "Aperçu du lien", + "links_count": "Liens actifs", + "links_sum": "Ouvrir les liens et les QR codes", + "no-account": "Vous n´avez pas encore de compte Gradido?", + "no-redeem": "Vous n´êtes pas autorisé à percevoir votre propre lien!", + "not-copied": "Malheureusement votre appareil ne permet pas de copier! Veuillez copier le lien manuellement svp!", + "redeem": "Encaisser", + "redeem-text": "Voulez-vous percevoir le montant maintenant?", + "redeemed": "Encaissé avec succès! Votre compte est crédité de {n} GDD.", + "redeemed-at": "Le lien a déjà été perçu le {date}.", + "redeemed-title": "encaisser", + "to-login": "Connexion", + "to-register": "Enregistrer un nouveau compte.", + "validUntil": "Valide jusqu´au", + "validUntilDate": "Le lien est valide jusqu´au {date}." + }, + "gdt": { + "calculation": "Calcul de Gradido Transform", + "contribution": "Contribution", + "conversion": "Conversion", + "conversion-gdt-euro": "Conversion Euro / Gradido Transform (GDT)", + "credit": "Crédit", + "factor": "Coefficient", + "formula": "Formule de calcul", + "funding": "Aux contributions au financement", + "gdt": "Gradido Transform", + "gdt-received": "Gradido Transform (GDT) perçu", + "no-transactions": "Vous ne possédez pas encore Gradido Transform (GDT).", + "not-reachable": "Le Serveur GDT n´est pas accessible.", + "publisher": "Un membre que vous avez référé a apporté un contribution", + "raise": "Augmentation", + "recruited-member": "Membre invité" + }, + "language": "Langage", + "link-load": "Enregistrer le dernier lien | Enregistrer les derniers {n} liens | Enregistrer plus de {n} liens", + "login": "Connexion", + "math": { + "aprox": "~", + "asterisk": "*", + "equal": "=", + "minus": "−", + "pipe": "|" + }, + "message": { + "activateEmail": "Votre compte n´a pas encore été activé. Veuillez vérifier vos emails et cliquer sur le lien d´activation ou faites la demande d´un nouveau lien en utilisant la page qui permet de générer un nouveau mot de passe.", + "checkEmail": "Votre email a bien été vérifié. Vous pouvez vous enregistrer maintenant.", + "email": "Nous vous avons envoyé un email.", + "errorTitle": "Attention!", + "register": "Vous êtes enregistré maintenant, merci de vérifier votre boîte mail et cliquer sur le lien d´activation.", + "reset": "Votre mot de passe a été modifié.", + "title": "Merci!", + "unsetPassword": "Votre mot de passe n´a pas été accepté. Merci de le réinitialiser." + }, + "navigation": { + "admin_area": "Partie administrative", + "community": "Communauté", + "info": "Information", + "logout": "Déconnexion", + "members_area": "Partie réservée aux membres", + "overview": "Aperçu", + "profile": "Mon profile", + "send": "Envoyer", + "support": "Aide", + "transactions": "Transactions" + }, + "qrCode": "QR Code", + "send_gdd": "Envoyer GDD", + "send_per_link": "Envoyer GDD via lien", + "session": { + "extend": "Rester connecter", + "lightText": "S´il n´apparaît aucune activité pendant plus de 10 minutes, la session expirera pour des raisons de sécurité.", + "logoutIn": "Se déconnecter ", + "warningText": "Êtes-vous toujours connecté?" + }, + "settings": { + "language": { + "changeLanguage": "Changer la langue", + "de": "Allemand", + "en": "Anglais", + "es": "Espagnol", + "fr": "Français", + "nl": "Néerlandais", + "success": "Votre langue de préférence a bien été actualisée." + }, + "name": { + "change-name": "Changer le nom", + "change-success": "Votre nom a bien été changé." + }, + "newsletter": { + "newsletter": "Information par email", + "newsletterFalse": "Vous ne recevrez aucune information par email.", + "newsletterTrue": "Vous recevrez de l´information par email." + }, + "password": { + "change-password": "Changer le mot de passe", + "forgot_pwd": "Mot de passe oublié?", + "resend_subtitle": "Votre lien d´activation a expiré, vous pouvez en obtenir un nouveau ici.", + "reset": "Réinitialiser le mot de passe", + "reset-password": { + "text": "Entrez un nouveau mot de passe que vous utiliserez dans le futur pour vous connecter à votre compte Gradido.." + }, + "send_now": "Envoyer maintenant", + "set": "Définir le mot de passe", + "set-password": { + "text": "Sauvegardez votre nouveau mot de passe maintenant, que vous pourrez utiliser pour vous connecter à votre compte Gradido dans le futur." + }, + "subtitle": "Si vous avez oublié votre mot de passe, vous pouvez le réinitialiser ici." + } + }, + "signin": "S´identifier", + "signup": "S´inscrire", + "site": { + "forgotPassword": { + "heading": "Veuillez entrer l´adresse email sous laquelle vous êtes enregistré ici svp." + }, + "login": { + "heading": "Vous connecter avec vos données d´accès. Gardez les en sécurité!" + }, + "resetPassword": { + "heading": "Entrez votre mot de passe et répétez l´action svp." + }, + "signup": { + "agree": "J´accepte le politique de confidentialité .", + "dont_match": "Les mots de passe ne correspondent pas.", + "heading": "Vous enregistrer en entrant toutes les données demandées dans les champs requis.", + "lowercase": "Une lettre minuscule est requise.", + "minimum": "8 caractères minimum.", + "no-whitespace": "Pas d´espace ni d´onglet", + "one_number": "Un chiffre requis.", + "special-char": "Un caractère spécial requis (e.g. _ or ä)", + "uppercase": "Une lettre majuscule requise." + } + }, + "statistic": { + "activeUsers": "Membres actifs", + "deletedUsers": "Membres supprimés", + "totalGradidoAvailable": "GDD total en circulation", + "totalGradidoCreated": "GDD total puisé", + "totalGradidoDecayed": "Total de GDD écoulé", + "totalGradidoUnbookedDecayed": "Total GDD non comptabilisé écoulé" + }, + "success": "Avec succès", + "time": { + "days": "Jours", + "hours": "Heures", + "minutes": "Minutes", + "month": "Moi", + "months": "Mois", + "seconds": "Secondes", + "years": "Année" + }, + "transaction": { + "gdd-text": "Transactions Gradido", + "gdt-text": "Transactions de GradidoTransform", + "nullTransactions": "Vous n´avez pas encore de transaction effectuée sur votre compte.", + "receiverDeleted": "Le compte du destinataire n´existe plus", + "receiverNotFound": "Destinataire inconnu", + "show_all": "Voir toutes les {count} transactions." + }, + "transaction-link": { + "send_you": "veut vous envoyer" + }, + "via_link": "par lien", + "welcome": "Bienvenu dans la communauté" +} diff --git a/frontend/src/locales/index.js b/frontend/src/locales/index.js index 4cb375b40..aa27a2e32 100644 --- a/frontend/src/locales/index.js +++ b/frontend/src/locales/index.js @@ -11,6 +11,24 @@ const locales = [ iso: 'de-DE', enabled: true, }, + { + name: 'Español', + code: 'es', + iso: 'es-ES', + enabled: true, + }, + { + name: 'Français', + code: 'fr', + iso: 'fr-FR', + enabled: true, + }, + { + name: 'Holandés', + code: 'nl', + iso: 'nl-NL', + enabled: true, + }, ] export default locales diff --git a/frontend/src/locales/nl.json b/frontend/src/locales/nl.json new file mode 100644 index 000000000..38c5b9c1a --- /dev/null +++ b/frontend/src/locales/nl.json @@ -0,0 +1,342 @@ +{ + "100": "100%", + "1000thanks": "1000 dank, omdat je bij ons bent!", + "125": "125%", + "85": "85%", + "advanced-calculation": "Voorcalculatie", + "auth": { + "left": { + "dignity": "Waardigheid", + "donation": "Gift", + "gratitude": "Dankbaarheid", + "hasAccount": "Je hebt al een rekening?", + "hereLogin": "Hier aanmelden", + "learnMore": "Meer ervaren …", + "oneDignity": "We geven aan elkaar en bedanken met Gradido.", + "oneDonation": "Jij bent een geschenk voor de gemeenschap. 1000 dank dat je bij ons bent.", + "oneGratitude": "Voor elkaar, voor alle mensen, voor de natuur." + }, + "navbar": { + "aboutGradido": "Over Gradido" + } + }, + "back": "Terug", + "community": { + "choose-another-community": "Kies een andere gemeenschap", + "community": "Gemeenschap", + "continue-to-registration": "Verder ter registratie", + "current-community": "Actuele gemeenschap", + "members": "Leden", + "moderators": "Moderators", + "myContributions": "Mijn bijdragen voor het algemeen belang", + "openContributionLinks": "openbare lijst van bijdragen", + "openContributionLinkText": "De volgende {count} automatische creaties worden momenteel aangeboden door de gemeenschap \"{name}\".", + "other-communities": "Verdere gemeenschappen", + "statistic": "Statistieken", + "submitContribution": "Bijdrage indienen", + "switch-to-this-community": "naar deze gemeenschap wisselen" + }, + "contribution": { + "activity": "Activiteit", + "alert": { + "communityNoteList": "Hier vind je alle ingediende en bevestigde bijdragen van alle leden uit deze gemeenschap.", + "confirm": "bevestigt", + "myContributionNoteList": "Ingediende bijdragen, die nog niet bevestigd zijn, kun je op elk moment wijzigen of verwijderen.", + "myContributionNoteSupport": "Hier heb je binnenkort de mogelijkheid een gesprek met een moderator te voeren. Mocht je nu problemen hebben, dan neem alsjeblieft contact op met Support.", + "pending": "Ingediend en wacht op bevestiging", + "rejected": "afgewezen" + }, + "date": "Bijdrage voor:", + "delete": "Bijdrage verwijderen! Weet je het zeker?", + "deleted": "De bijdrage werd verwijderd! Blijft echter zichtbaar.", + "formText": { + "bringYourTalentsTo": "Bied je met jouw talenten in de gemeenschap aan! Jouw vrijwillige inzet belonen we met 20 GGD per uur, tot maximaal 1.000 GGD per maand.", + "describeYourCommunity": "Beschrijf jouw activiteit voor het algemeen belang met vermelding van het aantal uren en registreer een bedrag van 20 GDD per uur! Na bevestiging door een moderator wordt het bedrag op jouw rekening bijgeschreven.", + "maxGDDforMonth": "Je kunt voor de geselecteerde maand nog maximaal {amount} GDD indienen.", + "openAmountForMonth": "Voor {monthAndYear} kun je nog {creation} GDD indienen.", + "yourContribution": "Jouw bijdrage voor het algemeen belang" + }, + "noDateSelected": "Kies een willekeurige datum in de maand", + "selectDate": "Wanneer was jouw bijdrage?", + "submit": "Indienen", + "submitted": "De bijdrage werd ingediend.", + "updated": "De bijdrage werd veranderd.", + "yourActivity": "Voer een activiteit in!" + }, + "contribution-link": { + "thanksYouWith": "bedankt jou met", + "unique": "(uniek)" + }, + "decay": { + "before_startblock_transaction": "Deze transactie heeft geen vergankelijkheid.", + "calculation_decay": "Berekening van de vergankelijkheid", + "calculation_total": "Berekening van het totaalbedrag", + "decay": "Vergankelijkheid", + "decay_introduced": "De vergankelijkheid werd ingevoerd op:", + "decay_since_last_transaction": "Vergankelijkheid sinds de laatste transactie", + "last_transaction": "Laatste transactie", + "past_time": "Verlopen tijd", + "Starting_block_decay": "Startblok vergankelijkheid", + "total": "Totaal", + "types": { + "creation": "Gecreëerd", + "noDecay": "Geen vergankelijkheid", + "receive": "Ontvangen", + "send": "Verstuurd" + } + }, + "delete": "Verwijderen", + "em-dash": "—", + "error": { + "email-already-sent": "Wij hebben jou minder dan 10 minuten geleden een email gestuurd.", + "empty-transactionlist": "Er was een fout met de overdracht van het aantal van jouw transacties.", + "error": "Fout!", + "no-account": "Helaas kunnen we geen (geactiveerde) rekening met deze gegevens vinden.", + "no-transactionlist": " Helaas was er een fout. Door de server werden er geen transacties overgedragen.", + "no-user": "Geen gebruiker met deze login gegevens.", + "session-expired": "De sessie werd om veiligheidsredenen beëindigd.", + "unknown-error": "Onbekende fout: " + }, + "followUs": "volg ons:", + "footer": { + "app_version": "app versie {version}", + "copyright": { + "link": "Gradido-Academie", + "year": "© {year}" + }, + "imprint": "Colofon", + "privacy_policy": "Privacyverklaring", + "short_hash": "({shortHash})", + "whitepaper": "Witboek" + }, + "form": { + "amount": "Bedrag", + "at": "op", + "cancel": "Annuleren", + "change": "Wijzigen", + "check_now": "Nu controleren", + "close": "Sluiten", + "current_balance": "Actueel banksaldo", + "date": "Datum", + "description": "Beschrijving", + "email": "Email", + "firstname": "Voornaam", + "from": "Van", + "generate_now": "Nu genereren", + "lastname": "Achternaam", + "mandatoryField": "verplicht veld", + "memo": "Memo", + "message": "Bericht", + "new_balance": "Nieuw banksaldo na bevestiging", + "no_gdd_available": "Je hebt geen GDD om te versturen.", + "password": "Wachtwoord", + "passwordRepeat": "Wachtwoord herhalen", + "password_new": "Nieuw wachtwoord", + "password_new_repeat": "Nieuw wachtwoord herhalen", + "password_old": "Oud wachtwoord", + "recipient": "Ontvanger", + "reset": "Resetten", + "save": "Opslaan", + "scann_code": "QR Code Scanner - Scan de QR Code van uw partner", + "sender": "Afzender", + "send_check": "Bevestig jouw transactie. Controleer alsjeblieft nogmaals alle gegevens!", + "send_now": "Nu versturen", + "send_transaction_error": "Helaas kon de transactie niet uitgevoerd worden!", + "send_transaction_success": " Jouw transactie werd succesvol uitgevoerd ", + "sorry": "Sorry", + "thx": "Dankjewel", + "time": "Tijd", + "to": "tot", + "to1": "aan", + "validation": { + "gddSendAmount": "Het veld {_field_} moet een getal tussen {min} en {max} met maximaal twee cijfers achter de komma zijn", + "is-not": "Je kunt geen Gradidos aan jezelf overmaken", + "usernmae-regex": "De gebruikersnaam moet met een letter beginnen, waarop minimaal twee alfanumerieke tekens dienen te volgen.", + "usernmae-unique": "De gebruikersnaam is al bezet." + }, + "your_amount": "Jouw bijdrage" + }, + "GDD": "GDD", + "gdd_per_link": { + "choose-amount": "Kies een bedrag dat je per link versturen wil. Je kunt ook nog een bericht invullen. Wanneer je „Nu genereren“ klikt, wordt er een link gecreëerd die je kunt versturen.", + "copy-link": "Link kopiëren", + "copy-link-with-text": "Link en tekst kopiëren", + "created": "De link werd gecreëerd!", + "credit-your-gradido": "Om de Gradidos bijgeschreven te krijgen, klik op de link!", + "decay-14-day": "Vergankelijkheid voor 14 dagen", + "delete-the-link": "De link verwijderen?", + "deleted": "De link werd verwijderd!", + "expiredOn": "Afgelopen op", + "has-account": "Heb je al een Gradido rekening?", + "header": "Gradidos per link versturen", + "isFree": "Gradido is gratis wereldwijd.", + "link-and-text-copied": "De link en jouw bericht werden naar het klembord gekopieerd. Je kunt ze nu in een email of bericht invoegen.", + "link-copied": "Link werd naar het klembord gekopieerd. Je kunt deze nu in een email of bericht invoegen.", + "link-deleted": "De link werd op {date} verwijderd.", + "link-expired": "De link is niet meer geldig. De geldigheid is op {date} afgelopen.", + "link-hint": "Attentie: Iedereen kan deze link inwisselen. Geef het alsjeblieft niet door!", + "link-overview": "Overzicht links", + "links_count": "Actieve links", + "links_sum": "Open links en QR-Codes", + "no-account": "Je hebt nog geen Gradido rekening?", + "no-redeem": "Je mag je eigen link niet inwisselen!", + "not-copied": "Jouw apparaat laat het kopiëren helaas niet toe! Kopieer de link alsjeblieft met de hand!", + "redeem": "Inwisselen", + "redeem-text": "Wil je het bedrag nu inwisselen?", + "redeemed": "Succesvol ingewisseld! Op jouw rekening werden {n} GDD bijgeschreven.", + "redeemed-at": "De link werd al op {date} ingewisseld.", + "redeemed-title": "ingewisseld", + "to-login": "Inloggen", + "to-register": "Registreer een nieuwe rekening.", + "validUntil": "Geldig tot", + "validUntilDate": "De link is geldig tot {date}." + }, + "gdt": { + "calculation": "Berekening van de Gradido Transform", + "contribution": "Bedrag", + "conversion": "Omrekening", + "conversion-gdt-euro": "Omrekening Euro / Gradido Transform (GDT)", + "credit": "Krediet", + "factor": "Factor", + "formula": "Berekeningsformule", + "funding": "Naar de donaties", + "gdt": "Gradido Transform", + "gdt-received": "Gradido Transform (GDT) ontvangen", + "no-transactions": "Je hebt nog geen Gradido Transform (GDT).", + "not-reachable": "De GDT server is niet bereikbaar.", + "publisher": "Jouw geworven lid heeft een bijdrage betaald ", + "raise": "Verhoging", + "recruited-member": "Uitgenodigd lid" + }, + "language": "Taal", + "link-load": "de laatste link herladen | de laatste links herladen | verdere {n} links herladen", + "login": "Aanmelding", + "math": { + "aprox": "~", + "asterisk": "*", + "equal": "=", + "minus": "−", + "pipe": "|" + }, + "message": { + "activateEmail": "Jouw rekening werd nog niet geactiveerd. Controleer aljeblieft jouw email en klik de activeringslink. Of vraag een nieuwe activeringlink via de Wachtwoord-Reset-Pagina aan.", + "checkEmail": "Jouw email werd succesvol geverifieerd. Je kunt je nu aanmelden.", + "email": "We hebben jou een email gestuurd.", + "errorTitle": "Opgelet!", + "register": "Je bent nu geregistreerd. Controleer alsjeblieft je emails en klik op de activeringslink.", + "reset": "Jouw wachtwoord werd gewijzigd.", + "title": "Dankjewel!", + "unsetPassword": "Jouw wachtwoord werd nog niet ingesteld. Doe het alsjeblieft opnieuw." + }, + "navigation": { + "admin_area": "Beheerder", + "community": "Gemeenschap", + "info": "Informatie", + "logout": "Afmelden", + "members_area": "Ledenbestand", + "overview": "Overzicht", + "profile": "Mijn profiel", + "send": "Sturen", + "support": "Support", + "transactions": "Transacties" + }, + "qrCode": "QR Code", + "send_gdd": "GDD sturen", + "send_per_link": "GDD per link versturen", + "session": { + "extend": "Aangemeld blijven", + "lightText": "Wanneer je langer dan 10 minuten geen actie ondernomen hebt, word je om veiligheidsredenen afgemeld.", + "logoutIn": "Afmelden in ", + "warningText": "Ben je er nog?" + }, + "settings": { + "language": { + "changeLanguage": "Taal veranderen", + "de": "Duits", + "en": "Engels", + "es": "Spaans", + "fr": "Frans", + "nl": "Nederlands", + "success": "Jouw taal werd succesvol veranderd." + }, + "name": { + "change-name": "Naam veranderen", + "change-success": "Jouw naam werd succesvol veranderd." + }, + "newsletter": { + "newsletter": "Informatie per email", + "newsletterFalse": "Je ontvangt geen informatie per email.", + "newsletterTrue": "Je ontvangt informatie per email." + }, + "password": { + "change-password": "Wachtwoord veranderen", + "forgot_pwd": "Wachtwoord vergeten?", + "resend_subtitle": "Jouw activeringslink is afgelopen. Je kunt hier een nieuwe aanvragen.", + "reset": "Wachtwoord opnieuw instellen", + "reset-password": { + "text": "Stel een nieuw wachtwoord in, waarmee je je voortaan in jouw Gradido-rekening wilt aanmelden." + }, + "send_now": "Nu versturen", + "set": "Wachtwoord instellen", + "set-password": { + "text": "Sla nu je nieuwe wachtwoord, waarmee je je voortaan in jouw Gradido-rekening kunt aanmelden, op." + }, + "subtitle": "Wanneer je het wachtwoord hebt vergeten, kun je het hier opnieuw instellen." + } + }, + "signin": "Aanmelden", + "signup": "Registreren", + "site": { + "forgotPassword": { + "heading": "Geef alsjeblieft jouw email, waarmee je bij Gradido aangemeld bent." + }, + "login": { + "heading": "Meld je met jouw inloggegevens aan. Sla deze altijd veilig op!" + }, + "resetPassword": { + "heading": "Vul alsjeblieft jouw wachtwoord in, en herhaal het." + }, + "signup": { + "agree": "Ik ga akkoord met Datenschutzerklärung.", + "dont_match": "De wachtwoorden zijn niet gelijk.", + "heading": "Schrijf je in door alle gegevens volledig en in de juiste velden in te vullen.", + "lowercase": "Een kleine letter is noodzakelijk.", + "minimum": "Minstens 8 tekens.", + "no-whitespace": "Geen spaties en tabs", + "one_number": "Getal noodzakelijk.", + "special-char": "Speciaal teken noodzakelijk (bijv. _ of é)", + "uppercase": "Hoofdletter noodzakelijk." + } + }, + "statistic": { + "activeUsers": "Actieve leden", + "deletedUsers": "Verwijderde leden", + "totalGradidoAvailable": "Totaal GDD in omloop", + "totalGradidoCreated": "Totaal GDD geschept", + "totalGradidoDecayed": "Totaal GDD vervallen", + "totalGradidoUnbookedDecayed": "Totaal niet geboekte GDD vervallen" + }, + "success": "Succes", + "time": { + "days": "Dagen", + "hours": "Uren", + "minutes": "Minuten", + "month": "Maand", + "months": "Maanden", + "seconds": "Seconden", + "years": "Jaar" + }, + "transaction": { + "gdd-text": "Gradido transacties", + "gdt-text": "GradidoTransform transacties", + "nullTransactions": "Je hebt nog geen transacties op jouw rekening.", + "receiverDeleted": "De rekening van de ontvanger werd verwijderd", + "receiverNotFound": "Ontvanger niet gevonden", + "show_all": "Alle {count} Transacties bekijken." + }, + "transaction-link": { + "send_you": "stuurt jou" + }, + "via_link": "via een link", + "welcome": "Welkom in de gemeenschap" +} diff --git a/frontend/src/mixins/copyLinks.js b/frontend/src/mixins/copyLinks.js new file mode 100644 index 000000000..ac6be95c5 --- /dev/null +++ b/frontend/src/mixins/copyLinks.js @@ -0,0 +1,45 @@ +export const copyLinks = { + props: { + link: { type: String, required: true }, + amount: { type: String, required: true }, + memo: { type: String, required: true }, + validUntil: { type: String, required: true }, + }, + data() { + return { + canCopyLink: true, + } + }, + methods: { + copyLink() { + navigator.clipboard + .writeText(this.link) + .then(() => { + this.toastSuccess(this.$t('gdd_per_link.link-copied')) + }) + .catch(() => { + this.canCopyLink = false + this.toastError(this.$t('gdd_per_link.not-copied')) + }) + }, + copyLinkWithText() { + navigator.clipboard + .writeText( + `${this.link} +${this.$store.state.firstName} ${this.$t('transaction-link.send_you')} ${this.amount} Gradido. +"${this.memo}" +${this.$t('gdd_per_link.credit-your-gradido')} ${this.$t('gdd_per_link.validUntilDate', { + date: this.$d(new Date(this.validUntil), 'short'), + })} +${this.$t('gdd_per_link.link-hint')}`, + ) + .then(() => { + this.toastSuccess(this.$t('gdd_per_link.link-and-text-copied')) + }) + .catch(() => { + this.canCopyLink = false + this.toastError(this.$t('gdd_per_link.not-copied')) + }) + }, + }, +} diff --git a/frontend/src/pages/Community.spec.js b/frontend/src/pages/Community.spec.js new file mode 100644 index 000000000..b4aa43785 --- /dev/null +++ b/frontend/src/pages/Community.spec.js @@ -0,0 +1,408 @@ +import { mount } from '@vue/test-utils' +import Community from './Community' +import { toastErrorSpy, toastSuccessSpy } from '@test/testSetup' +import { createContribution, updateContribution, deleteContribution } from '@/graphql/mutations' +import { listContributions, listAllContributions, verifyLogin } from '@/graphql/queries' + +const localVue = global.localVue + +const mockStoreDispach = jest.fn() +const apolloQueryMock = jest.fn() +const apolloMutationMock = jest.fn() + +describe('Community', () => { + let wrapper + + const mocks = { + $t: jest.fn((t) => t), + $d: jest.fn((d) => d), + $apollo: { + query: apolloQueryMock, + mutate: apolloMutationMock, + }, + $store: { + dispatch: mockStoreDispach, + state: { + creation: ['1000', '1000', '1000'], + }, + }, + $i18n: { + locale: 'en', + }, + } + + const Wrapper = () => { + return mount(Community, { + localVue, + mocks, + }) + } + + describe('mount', () => { + beforeEach(() => { + apolloQueryMock.mockResolvedValue({ + data: { + listContributions: { + contributionList: [ + { + id: 1555, + amount: '200', + memo: 'Fleisig, fleisig am Arbeiten mein lieber Freund, 50 Zeichen sind viel', + createdAt: '2022-07-15T08:47:06.000Z', + deletedAt: null, + confirmedBy: null, + confirmedAt: null, + }, + ], + contributionCount: 1, + }, + listAllContributions: { + contributionList: [ + { + id: 1555, + amount: '200', + memo: 'Fleisig, fleisig am Arbeiten mein lieber Freund, 50 Zeichen sind viel', + createdAt: '2022-07-15T08:47:06.000Z', + deletedAt: null, + confirmedBy: null, + confirmedAt: null, + }, + { + id: 1556, + amount: '400', + memo: 'Ein anderer lieber Freund ist auch sehr felißig am Arbeiten!!!!', + createdAt: '2022-07-16T08:47:06.000Z', + deletedAt: null, + confirmedBy: null, + confirmedAt: null, + }, + ], + contributionCount: 2, + }, + }, + }) + wrapper = Wrapper() + }) + + it('has a DIV .community-page', () => { + expect(wrapper.find('div.community-page').exists()).toBe(true) + }) + + describe('tabs', () => { + it('has three tabs', () => { + expect(wrapper.findAll('div[role="tabpanel"]')).toHaveLength(3) + }) + + it('has first tab active by default', () => { + expect(wrapper.findAll('div[role="tabpanel"]').at(0).classes('active')).toBe(true) + }) + }) + + describe('API calls after creation', () => { + it('emits update transactions', () => { + expect(wrapper.emitted('update-transactions')).toEqual([[0]]) + }) + + it('queries list of own contributions', () => { + expect(apolloQueryMock).toBeCalledWith({ + fetchPolicy: 'no-cache', + query: listContributions, + variables: { + currentPage: 1, + pageSize: 25, + }, + }) + }) + + it('queries list of all contributions', () => { + expect(apolloQueryMock).toBeCalledWith({ + fetchPolicy: 'no-cache', + query: listAllContributions, + variables: { + currentPage: 1, + pageSize: 25, + }, + }) + }) + + describe('server response is error', () => { + beforeEach(() => { + jest.clearAllMocks() + apolloQueryMock.mockRejectedValue({ message: 'Ups' }) + wrapper = Wrapper() + }) + + it('toasts two errors', () => { + expect(toastErrorSpy).toBeCalledTimes(2) + expect(toastErrorSpy).toBeCalledWith('Ups') + }) + }) + }) + + describe('set contrubtion', () => { + describe('with success', () => { + const now = new Date().toISOString() + beforeEach(async () => { + jest.clearAllMocks() + apolloMutationMock.mockResolvedValue({ + data: { + createContribution: true, + }, + }) + await wrapper.setData({ + form: { + id: null, + date: now, + memo: 'Mein Beitrag zur Gemeinschaft für diesen Monat ...', + amount: '200', + }, + }) + await wrapper.find('form').trigger('submit') + }) + + it('calls the create contribution mutation', () => { + expect(apolloMutationMock).toBeCalledWith({ + fetchPolicy: 'no-cache', + mutation: createContribution, + variables: { + creationDate: now, + memo: 'Mein Beitrag zur Gemeinschaft für diesen Monat ...', + amount: '200', + }, + }) + }) + + it('toasts a success message', () => { + expect(toastSuccessSpy).toBeCalledWith('contribution.submitted') + }) + + it('updates the contribution list', () => { + expect(apolloQueryMock).toBeCalledWith({ + fetchPolicy: 'no-cache', + query: listContributions, + variables: { + currentPage: 1, + pageSize: 25, + }, + }) + }) + + it('verifies the login (to get the new creations available)', () => { + expect(apolloQueryMock).toBeCalledWith({ + query: verifyLogin, + fetchPolicy: 'network-only', + }) + }) + + it('set all data to the default values)', () => { + expect(wrapper.vm.form.id).toBe(null) + expect(wrapper.vm.form.date).toBe('') + expect(wrapper.vm.form.memo).toBe('') + expect(wrapper.vm.form.amount).toBe('') + }) + }) + + describe('with error', () => { + const now = new Date().toISOString() + beforeEach(async () => { + jest.clearAllMocks() + apolloMutationMock.mockRejectedValue({ + message: 'Ouch!', + }) + await wrapper.setData({ + form: { + id: null, + date: now, + memo: 'Mein Beitrag zur Gemeinschaft für diesen Monat ...', + amount: '200', + }, + }) + await wrapper.find('form').trigger('submit') + }) + + it('toasts the error message', () => { + expect(toastErrorSpy).toBeCalledWith('Ouch!') + }) + }) + }) + + describe('update contrubtion', () => { + describe('with success', () => { + const now = new Date().toISOString() + beforeEach(async () => { + jest.clearAllMocks() + apolloMutationMock.mockResolvedValue({ + data: { + updateContribution: true, + }, + }) + await wrapper + .findComponent({ name: 'ContributionForm' }) + .vm.$emit('update-contribution', { + id: 2, + date: now, + memo: 'Mein Beitrag zur Gemeinschaft für diesen Monat ...', + amount: '400', + }) + }) + + it('calls the update contribution mutation', () => { + expect(apolloMutationMock).toBeCalledWith({ + fetchPolicy: 'no-cache', + mutation: updateContribution, + variables: { + contributionId: 2, + creationDate: now, + memo: 'Mein Beitrag zur Gemeinschaft für diesen Monat ...', + amount: '400', + }, + }) + }) + + it('toasts a success message', () => { + expect(toastSuccessSpy).toBeCalledWith('contribution.updated') + }) + + it('updates the contribution list', () => { + expect(apolloQueryMock).toBeCalledWith({ + fetchPolicy: 'no-cache', + query: listContributions, + variables: { + currentPage: 1, + pageSize: 25, + }, + }) + }) + + it('verifies the login (to get the new creations available)', () => { + expect(apolloQueryMock).toBeCalledWith({ + query: verifyLogin, + fetchPolicy: 'network-only', + }) + }) + + it('set all data to the default values)', () => { + expect(wrapper.vm.form.id).toBe(null) + expect(wrapper.vm.form.date).toBe('') + expect(wrapper.vm.form.memo).toBe('') + expect(wrapper.vm.form.amount).toBe('') + }) + }) + + describe('with error', () => { + const now = new Date().toISOString() + beforeEach(async () => { + jest.clearAllMocks() + apolloMutationMock.mockRejectedValue({ + message: 'Oh No!', + }) + await wrapper + .findComponent({ name: 'ContributionForm' }) + .vm.$emit('update-contribution', { + id: 2, + date: now, + memo: 'Mein Beitrag zur Gemeinschaft für diesen Monat ...', + amount: '400', + }) + }) + + it('toasts the error message', () => { + expect(toastErrorSpy).toBeCalledWith('Oh No!') + }) + }) + }) + + describe('delete contribution', () => { + let contributionListComponent + + beforeEach(async () => { + await wrapper.setData({ tabIndex: 1 }) + contributionListComponent = await wrapper.findComponent({ name: 'ContributionList' }) + }) + + describe('with success', () => { + beforeEach(async () => { + jest.clearAllMocks() + apolloMutationMock.mockResolvedValue({ + data: { + deleteContribution: true, + }, + }) + contributionListComponent.vm.$emit('delete-contribution', { id: 2 }) + }) + + it('calls the API', () => { + expect(apolloMutationMock).toBeCalledWith({ + fetchPolicy: 'no-cache', + mutation: deleteContribution, + variables: { + id: 2, + }, + }) + }) + + it('toasts a success message', () => { + expect(toastSuccessSpy).toBeCalledWith('contribution.deleted') + }) + + it('updates the contribution list', () => { + expect(apolloQueryMock).toBeCalledWith({ + fetchPolicy: 'no-cache', + query: listContributions, + variables: { + currentPage: 1, + pageSize: 25, + }, + }) + }) + + it('verifies the login (to get the new creations available)', () => { + expect(apolloQueryMock).toBeCalledWith({ + query: verifyLogin, + fetchPolicy: 'network-only', + }) + }) + }) + + describe('with error', () => { + beforeEach(async () => { + jest.clearAllMocks() + apolloMutationMock.mockRejectedValue({ + message: 'Oh my god!', + }) + contributionListComponent.vm.$emit('delete-contribution', { id: 2 }) + }) + + it('toasts the error message', () => { + expect(toastErrorSpy).toBeCalledWith('Oh my god!') + }) + }) + }) + + describe('update contribution form', () => { + const now = new Date().toISOString() + beforeEach(async () => { + await wrapper.setData({ tabIndex: 1 }) + await wrapper + .findComponent({ name: 'ContributionList' }) + .vm.$emit('update-contribution-form', { + id: 2, + contributionDate: now, + memo: 'Mein Beitrag zur Gemeinschaft für diesen Monat ...', + amount: '400', + }) + }) + + it('sets the form data to the new values', () => { + expect(wrapper.vm.form.id).toBe(2) + expect(wrapper.vm.form.date).toBe(now) + expect(wrapper.vm.form.memo).toBe('Mein Beitrag zur Gemeinschaft für diesen Monat ...') + expect(wrapper.vm.form.amount).toBe('400') + }) + + it('sets tab index back to 0', () => { + expect(wrapper.vm.tabIndex).toBe(0) + }) + }) + }) +}) diff --git a/frontend/src/pages/Community.vue b/frontend/src/pages/Community.vue new file mode 100644 index 000000000..64aca6156 --- /dev/null +++ b/frontend/src/pages/Community.vue @@ -0,0 +1,276 @@ + + diff --git a/frontend/src/pages/InfoStatistic.spec.js b/frontend/src/pages/InfoStatistic.spec.js new file mode 100644 index 000000000..6adcf77d4 --- /dev/null +++ b/frontend/src/pages/InfoStatistic.spec.js @@ -0,0 +1,130 @@ +import { mount } from '@vue/test-utils' +import InfoStatistic from './InfoStatistic' +import { toastErrorSpy } from '../../test/testSetup' +import { listContributionLinks, communityStatistics, searchAdminUsers } from '@/graphql/queries' + +const localVue = global.localVue + +const apolloQueryMock = jest + .fn() + .mockResolvedValueOnce({ + data: { + listContributionLinks: { + count: 2, + links: [ + { + id: 1, + amount: 200, + name: 'Dokumenta 2017', + memo: 'Vielen Dank für deinen Besuch bei der Dokumenta 2017', + cycle: 'ONCE', + }, + { + id: 2, + amount: 200, + name: 'Dokumenta 2022', + memo: 'Vielen Dank für deinen Besuch bei der Dokumenta 2022', + cycle: 'ONCE', + }, + ], + }, + }, + }) + .mockResolvedValueOnce({ + data: { + searchAdminUsers: { + userCount: 2, + userList: [ + { firstName: 'Peter', lastName: 'Lustig' }, + { firstName: 'Super', lastName: 'Admin' }, + ], + }, + }, + }) + .mockResolvedValueOnce({ + data: { + communityStatistics: { + totalUsers: 3113, + activeUsers: 1057, + deletedUsers: 35, + totalGradidoCreated: '4083774.05000000000000000000', + totalGradidoDecayed: '-1062639.13634129622923372197', + totalGradidoAvailable: '2513565.869444365732411569', + totalGradidoUnbookedDecayed: '-500474.6738366222166261272', + }, + }, + }) + .mockResolvedValue('default') + +describe('InfoStatistic', () => { + let wrapper + + const mocks = { + $i18n: { + locale: 'en', + }, + $t: jest.fn((t) => t), + $apollo: { + query: apolloQueryMock, + }, + } + + const Wrapper = () => { + return mount(InfoStatistic, { localVue, mocks }) + } + + describe('mount', () => { + beforeEach(() => { + wrapper = Wrapper() + }) + + it('renders the info page', () => { + expect(wrapper.find('div.info-statistic').exists()).toBe(true) + }) + + it('calls listContributionLinks', () => { + expect(apolloQueryMock).toBeCalledWith( + expect.objectContaining({ + query: listContributionLinks, + fetchPolicy: 'network-only', + }), + ) + }) + + it('calls searchAdminUsers', () => { + expect(apolloQueryMock).toBeCalledWith( + expect.objectContaining({ + query: searchAdminUsers, + fetchPolicy: 'network-only', + }), + ) + }) + + it('calls getCommunityStatistics', () => { + expect(apolloQueryMock).toBeCalledWith( + expect.objectContaining({ + query: communityStatistics, + fetchPolicy: 'network-only', + }), + ) + }) + + describe('error apolloQueryMock', () => { + beforeEach(async () => { + jest.clearAllMocks() + apolloQueryMock.mockRejectedValue({ + message: 'uups', + }) + wrapper = Wrapper() + }) + + it('toasts three error messages', () => { + expect(toastErrorSpy).toBeCalledWith( + 'listContributionLinks has no result, use default data', + ) + expect(toastErrorSpy).toBeCalledWith('searchAdminUsers has no result, use default data') + expect(toastErrorSpy).toBeCalledWith('communityStatistics has no result, use default data') + }) + }) + }) +}) diff --git a/frontend/src/pages/InfoStatistic.vue b/frontend/src/pages/InfoStatistic.vue new file mode 100644 index 000000000..3d38e730a --- /dev/null +++ b/frontend/src/pages/InfoStatistic.vue @@ -0,0 +1,166 @@ + + diff --git a/frontend/src/pages/Send.spec.js b/frontend/src/pages/Send.spec.js index 47a30ff65..c1b6fb635 100644 --- a/frontend/src/pages/Send.spec.js +++ b/frontend/src/pages/Send.spec.js @@ -25,9 +25,11 @@ describe('Send', () => { const mocks = { $t: jest.fn((t) => t), $n: jest.fn((n) => String(n)), + $d: jest.fn((d) => d), $store: { state: { email: 'sender@example.org', + firstName: 'Testy', }, }, $apollo: { @@ -160,11 +162,15 @@ describe('Send', () => { }) describe('transaction form link', () => { + const now = new Date().toISOString() beforeEach(async () => { apolloMutationMock.mockResolvedValue({ data: { createTransactionLink: { link: 'http://localhost/redeem/0123456789', + amount: '56.78', + memo: 'Make the best of the link!', + validUntil: now, }, }, }) @@ -228,18 +234,65 @@ describe('Send', () => { navigator.clipboard = navigatorClipboard }) - describe('copy with success', () => { + describe('copy link with success', () => { beforeEach(async () => { navigatorClipboardMock.mockResolvedValue() - await wrapper.findAll('button').at(0).trigger('click') + await wrapper.findAll('button').at(1).trigger('click') }) + it('should call clipboard.writeText', () => { + expect(navigator.clipboard.writeText).toHaveBeenCalledWith( + 'http://localhost/redeem/0123456789', + ) + }) it('toasts success message', () => { expect(toastSuccessSpy).toBeCalledWith('gdd_per_link.link-copied') }) }) - describe('copy with error', () => { + describe('copy link with error', () => { + beforeEach(async () => { + navigatorClipboardMock.mockRejectedValue() + await wrapper.findAll('button').at(1).trigger('click') + }) + + it('toasts error message', () => { + expect(toastErrorSpy).toBeCalledWith('gdd_per_link.not-copied') + }) + }) + }) + + describe('copy link and text with success', () => { + const navigatorClipboard = navigator.clipboard + beforeAll(() => { + delete navigator.clipboard + navigator.clipboard = { writeText: navigatorClipboardMock } + }) + afterAll(() => { + navigator.clipboard = navigatorClipboard + }) + + describe('copy link and text with success', () => { + beforeEach(async () => { + navigatorClipboardMock.mockResolvedValue() + await wrapper.findAll('button').at(0).trigger('click') + }) + + it('should call clipboard.writeText', () => { + expect(navigator.clipboard.writeText).toHaveBeenCalledWith( + 'http://localhost/redeem/0123456789\n' + + 'Testy transaction-link.send_you 56.78 Gradido.\n' + + '"Make the best of the link!"\n' + + 'gdd_per_link.credit-your-gradido gdd_per_link.validUntilDate\n' + + 'gdd_per_link.link-hint', + ) + }) + it('toasts success message', () => { + expect(toastSuccessSpy).toBeCalledWith('gdd_per_link.link-and-text-copied') + }) + }) + + describe('copy link and text with error', () => { beforeEach(async () => { navigatorClipboardMock.mockRejectedValue() await wrapper.findAll('button').at(0).trigger('click') @@ -253,7 +306,7 @@ describe('Send', () => { describe('close button click', () => { beforeEach(async () => { - await wrapper.findAll('button').at(2).trigger('click') + await wrapper.findAll('button').at(3).trigger('click') }) it('Shows the TransactionForm', () => { diff --git a/frontend/src/pages/Send.vue b/frontend/src/pages/Send.vue index cd5f8f572..74e2b0270 100644 --- a/frontend/src/pages/Send.vue +++ b/frontend/src/pages/Send.vue @@ -41,7 +41,13 @@ >
@@ -144,7 +150,15 @@ export default { }) .then((result) => { this.$emit('set-tunneled-email', null) - this.link = result.data.createTransactionLink.link + const { + data: { + createTransactionLink: { link, amount, memo, validUntil }, + }, + } = result + this.link = link + this.amount = amount + this.memo = memo + this.validUntil = validUntil this.transactionData = { ...EMPTY_TRANSACTION_DATA } this.currentTransactionStep = TRANSACTION_STEPS.transactionResultLink this.updateTransactions({}) diff --git a/frontend/src/pages/TransactionLink.vue b/frontend/src/pages/TransactionLink.vue index 699c350ae..57236b55c 100644 --- a/frontend/src/pages/TransactionLink.vue +++ b/frontend/src/pages/TransactionLink.vue @@ -109,7 +109,7 @@ export default { return this.$route.params.code.search(/^CL-/) === 0 }, itemType() { - // link wurde gelöscht: am, von + // link is deleted: at, from if (this.linkData.deletedAt) { // eslint-disable-next-line vue/no-side-effects-in-computed-properties this.redeemedBoxText = this.$t('gdd_per_link.link-deleted', { diff --git a/frontend/src/pages/Transactions.spec.js b/frontend/src/pages/Transactions.spec.js index 94e0f51c1..171a089ca 100644 --- a/frontend/src/pages/Transactions.spec.js +++ b/frontend/src/pages/Transactions.spec.js @@ -6,6 +6,7 @@ import { toastErrorSpy } from '@test/testSetup' const localVue = global.localVue +const mockRouterReplace = jest.fn() const windowScrollToMock = jest.fn() window.scrollTo = windowScrollToMock @@ -39,6 +40,9 @@ describe('Transactions', () => { $apollo: { query: apolloMock, }, + $router: { + replace: mockRouterReplace, + }, } const Wrapper = () => { diff --git a/frontend/src/pages/Transactions.vue b/frontend/src/pages/Transactions.vue index 109e3f19c..d0f4ac8b8 100644 --- a/frontend/src/pages/Transactions.vue +++ b/frontend/src/pages/Transactions.vue @@ -1,7 +1,11 @@