diff --git a/frontend/app/cypress/integration/rating.spec.ts b/frontend/app/cypress/integration/rating.spec.ts new file mode 100644 index 00000000..67c943d9 --- /dev/null +++ b/frontend/app/cypress/integration/rating.spec.ts @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2023 StApps + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ +describe('ratings', function () { + beforeEach(() => { + cy.intercept('https://mobile.server.uni-frankfurt.de/rating', { + body: {}, + }).as('rating'); + cy.intercept('POST', 'https://mobile.server.uni-frankfurt.de/search', { + fixture: 'search/types/canteen/canteen-1.json', + }).as('search'); + cy.intercept('POST', 'https://mobile.server.uni-frankfurt.de/search/multi', { + fixture: 'search/types/dish/dish-1.json', + }); + }); + + it('should open ratings', function () { + cy.visit('/data-detail/86464b64-da1e-5578-a5c4-eec23457f596'); + cy.get('.rating-stars').should('not.exist'); + cy.get('stapps-rating').click({scrollBehavior: 'center'}); + cy.get('.rating-stars').should('exist'); + }); + + it('should submit ratings', function () { + cy.visit('/data-detail/86464b64-da1e-5578-a5c4-eec23457f596'); + cy.get('stapps-rating').click({scrollBehavior: 'center'}); + cy.get('.rating-stars > ion-icon').first().click({scrollBehavior: 'center'}); + cy.wait('@rating').its('request.body.rating').should('eq', 5); + }); + + it('should not be possible to rate twice', function () { + cy.visit('/data-detail/86464b64-da1e-5578-a5c4-eec23457f596'); + cy.get('stapps-rating').click({scrollBehavior: 'center'}); + cy.get('.rating-stars > ion-icon').first().click({scrollBehavior: 'center'}); + cy.wait('@rating'); + cy.get('stapps-rating ion-button').should('have.class', 'button-disabled'); + cy.visit('/data-detail/86464b64-da1e-5578-a5c4-eec23457f596'); + cy.get('stapps-rating ion-button').should('have.class', 'button-disabled'); + }); + + it('should display a thank you message', function () { + cy.visit('/data-detail/86464b64-da1e-5578-a5c4-eec23457f596'); + cy.get('stapps-rating').click({scrollBehavior: 'center'}); + cy.get('.rating-stars > ion-icon').first().click({scrollBehavior: 'center'}); + cy.wait('@rating'); + cy.get('.thank-you').should('be.visible'); + }); + + it('should be dismissible', function () { + cy.visit('/data-detail/86464b64-da1e-5578-a5c4-eec23457f596'); + cy.get('stapps-rating').click({scrollBehavior: 'center'}); + cy.get('.rating-stars').should('be.visible'); + cy.get('body').click(0, 0); + cy.get('.rating-stars').should('not.exist'); + }); +}); diff --git a/frontend/app/package.json b/frontend/app/package.json index 7c4036c8..86f86445 100644 --- a/frontend/app/package.json +++ b/frontend/app/package.json @@ -15,13 +15,13 @@ ], "scripts": { "analyze": "webpack-bundle-analyzer www/stats.json", - "build": "ng build --configuration=production --stats-json && webpack-bundle-analyzer www/stats.json --mode static --report www/bundle-info.html", + "build": "pnpm check-icons && ng build --configuration=production --stats-json && webpack-bundle-analyzer www/stats.json --mode static --report www/bundle-info.html", "build:analyze": "npm run build:stats && npm run analyze", "build:android": "ionic capacitor build android --no-open && cd android && ./gradlew clean assembleDebug && cd ..", "build:prod": "ng build --configuration=production", "build:stats": "ng build --configuration=production --stats-json", "changelog": "conventional-changelog -p angular -i src/assets/about/CHANGELOG.md -s -r 0", - "check-icons": "ts-node scripts/check-icon-correctness.ts", + "check-icons": "ts-node-esm scripts/check-icon-correctness.ts", "cypress:open": "cypress open", "cypress:run": "cypress run", "docker:build": "sudo docker run -p 8100:8100 -p 35729:35729 -p 53703:53703 -v $PWD:/app -it registry.gitlab.com/openstapps/app bash -c \"npm install && npm run build\"", @@ -36,7 +36,7 @@ "licenses": "license-checker --json > src/assets/about/licenses.json && ts-node ./scripts/accumulate-licenses.ts && git add src/assets/about/licenses.json", "lint": "ng lint", "lint:fix": "eslint --fix -c .eslintrc.json --ignore-path .eslintignore --ext .ts,.html src/", - "minify-icons": "ts-node scripts/minify-icon-font.ts", + "minify-icons": "ts-node-esm scripts/minify-icon-font.ts", "ng": "ng", "postinstall": "(jetify && node ngcc-postinstall.mjs) || echo \"skipping jetify in production mode\"", "preview": "http-server www --p 8101 -o", diff --git a/frontend/app/scripts/check-icon-correctness.ts b/frontend/app/scripts/check-icon-correctness.ts index 5582c8e1..42c06c16 100644 --- a/frontend/app/scripts/check-icon-correctness.ts +++ b/frontend/app/scripts/check-icon-correctness.ts @@ -27,6 +27,7 @@ const modifiedFont = fontkit.openSync(config.outputPath); let success = true; +// eslint-disable-next-line unicorn/prefer-top-level-await checkAll().then(() => { console.log(); if (success) { diff --git a/frontend/app/scripts/gather-used-icons.ts b/frontend/app/scripts/gather-used-icons.ts index 5b1fc2ea..bfd2f6e0 100644 --- a/frontend/app/scripts/gather-used-icons.ts +++ b/frontend/app/scripts/gather-used-icons.ts @@ -12,21 +12,16 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ -import glob from 'glob'; +import {glob} from 'glob'; import {readFileSync} from 'fs'; import {matchPropertyContent, matchTagProperties} from '../src/app/util/ion-icon/icon-match'; -const globPromise = (pattern: string) => - new Promise((resolve, reject) => - glob(pattern, (error, files) => (error ? reject(error) : resolve(files))), - ); - /** * */ -export async function getUsedIconsHtml(glob = 'src/**/*.html'): Promise> { +export async function getUsedIconsHtml(pattern = 'src/**/*.html'): Promise> { return Object.fromEntries( - (await globPromise(glob)) + (await glob(pattern)) .map(file => [ file, (readFileSync(file, 'utf8') @@ -43,9 +38,9 @@ export async function getUsedIconsHtml(glob = 'src/**/*.html'): Promise> { +export async function getUsedIconsTS(pattern = 'src/**/*.ts'): Promise> { return Object.fromEntries( - (await globPromise(glob)) + (await glob(pattern)) .map(file => [file, readFileSync(file, 'utf8').match(/(?<=Icon`)[\w-]+(?=`)/g) || []]) .filter(([, values]) => values.length > 0), ); diff --git a/frontend/app/scripts/minify-icon-font.ts b/frontend/app/scripts/minify-icon-font.ts index 9dfa8713..a8a44bf4 100644 --- a/frontend/app/scripts/minify-icon-font.ts +++ b/frontend/app/scripts/minify-icon-font.ts @@ -115,6 +115,7 @@ async function minifyIconFont() { ); } +// eslint-disable-next-line unicorn/prefer-top-level-await minifyIconFont(); /** diff --git a/frontend/app/scripts/tsconfig.json b/frontend/app/scripts/tsconfig.json index 7ddea8e9..821fd4bd 100644 --- a/frontend/app/scripts/tsconfig.json +++ b/frontend/app/scripts/tsconfig.json @@ -1,6 +1,7 @@ { - "extends": "../node_modules/@openstapps/configuration/tsconfig.json", + "extends": "@openstapps/tsconfig", "compilerOptions": { - "lib": ["es2019"] + "module": "CommonJS", + "moduleResolution": "Node" } } diff --git a/frontend/app/src/app/modules/assessments/list/assessments-list-item.html b/frontend/app/src/app/modules/assessments/list/assessments-list-item.html index 4c48af0a..b402b3a5 100644 --- a/frontend/app/src/app/modules/assessments/list/assessments-list-item.html +++ b/frontend/app/src/app/modules/assessments/list/assessments-list-item.html @@ -1,23 +1,23 @@ diff --git a/frontend/app/src/app/modules/data/data.module.ts b/frontend/app/src/app/modules/data/data.module.ts index 9949a445..2fa72694 100644 --- a/frontend/app/src/app/modules/data/data.module.ts +++ b/frontend/app/src/app/modules/data/data.module.ts @@ -98,6 +98,7 @@ import {PeriodicalDetailContentComponent} from './types/periodical/periodical-de import {DataListItemHostDirective} from './list/data-list-item-host.directive'; import {DataListItemHostDefaultComponent} from './list/data-list-item-host-default.component'; import {browserFactory, SimpleBrowser} from '../../util/browser.factory'; +import {StappsRatingComponent} from './elements/rating.component'; import {DishCharacteristicsComponent} from './types/dish/dish-characteristics.component'; import {SkeletonListComponent} from './list/skeleton-list.component'; @@ -155,6 +156,7 @@ import {SkeletonListComponent} from './list/skeleton-list.component'; SimpleCardComponent, SkeletonListItemComponent, SkeletonSegmentComponent, + StappsRatingComponent, SkeletonSimpleCardComponent, TreeListComponent, TreeListFragmentComponent, @@ -170,7 +172,6 @@ import {SkeletonListComponent} from './list/skeleton-list.component'; PeriodicalListItemComponent, PeriodicalDetailContentComponent, ], - entryComponents: [DataListComponent, SimpleDataListComponent], imports: [ CommonModule, DataRoutingModule, diff --git a/frontend/app/src/app/modules/data/elements/rating.animation.ts b/frontend/app/src/app/modules/data/elements/rating.animation.ts new file mode 100644 index 00000000..851afa20 --- /dev/null +++ b/frontend/app/src/app/modules/data/elements/rating.animation.ts @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2023 StApps + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ +import {animate, group, keyframes, query, stagger, style, transition, trigger} from '@angular/animations'; + +const duration = '800ms'; +const timingFunction = 'cubic-bezier(0.16, 1, 0.3, 1)'; +const timing = `${duration} ${timingFunction}`; +const staggerTiming = '75ms'; + +export const ratingAnimation = trigger('rating', [ + transition('rated => void', [ + style({opacity: 1, translate: '0 0'}), + group([ + query( + 'ion-icon:not(.rated-value)', + [ + style({translate: '0 0', opacity: 1}), + stagger(staggerTiming, animate(timing, style({translate: '0 -16px', opacity: 0}))), + ], + {optional: true}, + ), + query( + '.rated-value', + [ + style({translate: '0 0', opacity: 1}), + animate(`${duration} 200ms ${timingFunction}`, style({translate: '0 -16px', opacity: 0})), + ], + {optional: true}, + ), + query( + '.rated-value ~ .thank-you', + [ + style({translate: '0 16px', opacity: 0}), + animate(`${duration} 500ms ${timingFunction}`, style({translate: '0 0', opacity: 1})), + animate('1000ms', style({opacity: 1})), + ], + {optional: true}, + ), + ]), + animate(timing, style({opacity: 0, translate: '8px 0'})), + ]), + transition('abandoned => void', [ + style({opacity: 1, translate: '0 0'}), + animate(timing, style({opacity: 0, translate: '8px 0'})), + ]), + transition(':enter', [ + style({ + translate: '40% 0', + background: 'rgba(var(--ion-color-light), 0)', + }), + group([ + animate( + timing, + style({ + translate: '0 0', + background: 'rgba(var(--ion-color-light-rgb), 1)', + }), + ), + query('ion-icon', [ + style({ + translate: '16px 0', + scale: '0.4 0.2', + opacity: 0, + offset: 0, + willChange: 'scale, translate', + }), + stagger(staggerTiming, [ + animate( + timing, + keyframes([ + style({ + scale: 1.3, + offset: 0.2, + }), + style({ + translate: '0 0', + scale: '1 1', + opacity: 1, + offset: 1, + }), + ]), + ), + ]), + ]), + ]), + ]), +]); diff --git a/frontend/app/src/app/modules/data/elements/rating.component.ts b/frontend/app/src/app/modules/data/elements/rating.component.ts new file mode 100644 index 00000000..03f423cb --- /dev/null +++ b/frontend/app/src/app/modules/data/elements/rating.component.ts @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2023 StApps + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ +import {Component, ElementRef, HostListener, Input} from '@angular/core'; +import {SCDish, SCRatingRequest, SCUuid} from '@openstapps/core'; +import {RatingProvider} from '../rating.provider'; +import {ratingAnimation} from './rating.animation'; + +@Component({ + selector: 'stapps-rating', + templateUrl: 'rating.html', + styleUrls: ['rating.scss'], + animations: [ratingAnimation], +}) +export class StappsRatingComponent { + rate = false; + + rated = false; + + canBeRated = false; + + uid: SCUuid; + + rating?: number; + + @Input() set item(value: SCDish) { + this.uid = value.uid; + + Promise.all([this.ratingProvider.canRate(value), this.ratingProvider.hasRated(this.uid)] as const).then( + ([canRate, hasRated]) => { + this.canBeRated = canRate; + this.rated = hasRated; + }, + ); + } + + constructor(readonly elementRef: ElementRef, readonly ratingProvider: RatingProvider) {} + + async submitRating(rating: number) { + this.rating = rating; + try { + await this.ratingProvider.rate(this.uid, rating as SCRatingRequest['rating']); + this.rated = true; + } catch { + this.rating = undefined; + // allow change detection to catch up first + setTimeout(() => { + this.rate = false; + }); + } + } + + @HostListener('document:mousedown', ['$event']) + clickOutside(event: MouseEvent) { + if (this.rating) return; + if (!this.elementRef.nativeElement.contains(event.target)) { + this.rate = false; + } + } +} diff --git a/frontend/app/src/app/modules/data/elements/rating.html b/frontend/app/src/app/modules/data/elements/rating.html new file mode 100644 index 00000000..2248eb4b --- /dev/null +++ b/frontend/app/src/app/modules/data/elements/rating.html @@ -0,0 +1,36 @@ + + + + + + +
+ + +
diff --git a/frontend/app/src/app/modules/data/elements/rating.scss b/frontend/app/src/app/modules/data/elements/rating.scss new file mode 100644 index 00000000..3671225f --- /dev/null +++ b/frontend/app/src/app/modules/data/elements/rating.scss @@ -0,0 +1,66 @@ +/*! + * Copyright (C) 2023 StApps + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ +.rating-stars { + display: flex; + contain: content; + flex-direction: row-reverse; + justify-content: start; + align-items: center; + position: absolute; + background: var(--ion-color-light); + bottom: 0; + right: 0; + top: 0; + gap: var(--spacing-md); + padding-inline: var(--spacing-xl); + border-radius: var(--border-radius-default); + + > ion-icon { + @media (hover: hover) { + &:hover ~ *::ng-deep stapps-icon, + &:hover::ng-deep stapps-icon { + --fill: 1; + } + } + &:has(:checked)::ng-deep ~ *::ng-deep stapps-icon, + &:has(:checked)::ng-deep stapps-icon, + &:active::ng-deep ~ *::ng-deep stapps-icon, + &:active::ng-deep stapps-icon { + --fill: 1; + color: var(--ion-color-dark); + } + } + + > .thank-you { + opacity: 0; + pointer-events: none; + position: absolute; + right: 0; + top: 0; + bottom: 0; + padding: var(--spacing-md); + display: flex; + justify-content: center; + align-items: center; + } +} + +ion-button { + margin: 0; + + &.button-disabled::ng-deep stapps-icon { + --fill: 1; + } +} diff --git a/frontend/app/src/app/modules/data/list/data-list-item.component.ts b/frontend/app/src/app/modules/data/list/data-list-item.component.ts index 27bbf8d1..36d41818 100644 --- a/frontend/app/src/app/modules/data/list/data-list-item.component.ts +++ b/frontend/app/src/app/modules/data/list/data-list-item.component.ts @@ -36,7 +36,7 @@ export class DataListItemComponent { */ @Input() item: SCThings; - @Input() favoriteButton = true; + @Input() listItemEndInteraction = true; @Input() lines = 'inset'; diff --git a/frontend/app/src/app/modules/data/list/data-list-item.html b/frontend/app/src/app/modules/data/list/data-list-item.html index 3b785215..d8f1f562 100644 --- a/frontend/app/src/app/modules/data/list/data-list-item.html +++ b/frontend/app/src/app/modules/data/list/data-list-item.html @@ -32,7 +32,10 @@ - + + + + diff --git a/frontend/app/src/app/modules/data/list/data-list-item.scss b/frontend/app/src/app/modules/data/list/data-list-item.scss index aa4883a7..7649eea0 100644 --- a/frontend/app/src/app/modules/data/list/data-list-item.scss +++ b/frontend/app/src/app/modules/data/list/data-list-item.scss @@ -34,7 +34,7 @@ ion-item { @include border-radius-in-parallax(var(--border-radius-default)); overflow: hidden; --inner-padding-end: 0; - --padding-start: var(--spacing-sm); + --padding-start: 0; margin: var(--spacing-sm); ion-thumbnail { @@ -44,6 +44,7 @@ ion-item { ion-label { width: 100%; margin-right: 0; + padding-left: var(--spacing-sm); div { display: flex; @@ -68,11 +69,12 @@ ion-item { flex-basis: min-content; } + stapps-long-inline-text, .title { display: -webkit-box; white-space: break-spaces; -webkit-box-orient: vertical; - -webkit-line-clamp: 3; + -webkit-line-clamp: 2; overflow: hidden; } @@ -80,11 +82,33 @@ ion-item { display: none; } + stapps-rating, + stapps-favorite-button { + position: absolute; + bottom: 0; + right: 0; + } + + stapps-rating { + width: 100%; + + ion-button { + float: right; + } + + .rating-stars { + width: 100%; + justify-content: center; + gap: var(--spacing-xs); + padding-inline: 0; + } + } + // fix for Safari stapps-offers-in-list { position: absolute; bottom: 0; - right: 0; + left: 0; } stapps-offers-in-list .place { diff --git a/frontend/app/src/app/modules/data/rating.provider.ts b/frontend/app/src/app/modules/data/rating.provider.ts new file mode 100644 index 00000000..3841acc6 --- /dev/null +++ b/frontend/app/src/app/modules/data/rating.provider.ts @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2023 StApps + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ +import {Injectable} from '@angular/core'; +import { + SCAcademicPriceGroup, + SCDish, + SCISO8601Date, + SCRatingRequest, + SCRatingResponse, + SCRatingRoute, + SCUserGroup, + SCUserGroupSetting, + SCUuid, +} from '@openstapps/core'; +import {StAppsWebHttpClient} from './stapps-web-http-client.provider'; +import {StorageProvider} from '../storage/storage.provider'; +import {Client} from '@openstapps/api'; +import {environment} from '../../../environments/environment'; +import {SettingsProvider} from '../settings/settings.provider'; + +interface RatingStorage { + date: SCISO8601Date; + uuids: Record; +} + +@Injectable({ + providedIn: 'root', +}) +export class RatingProvider { + private readonly client: Client; + + private readonly backendUrl = environment.backend_url; + + private readonly appVersion = environment.backend_version; + + private readonly ratingRoute = new SCRatingRoute(); + + private readonly storageKey = 'rating.rated_uuids'; + + constructor( + stAppsWebHttpClient: StAppsWebHttpClient, + private readonly storageProvider: StorageProvider, + private readonly settingsProvider: SettingsProvider, + ) { + this.client = new Client(stAppsWebHttpClient, this.backendUrl, this.appVersion); + } + + private get today() { + const today = new Date(); + return new Date(today.getFullYear(), today.getMonth(), today.getDate()).toISOString(); + } + + private get userGroup(): Promise { + return this.settingsProvider + .getSetting('profile', 'group') + .then(it => (it as SCUserGroupSetting).value as SCUserGroup); + } + + private async getStoredRatings(): Promise { + const has = await this.storageProvider.has(this.storageKey); + const current = has + ? await this.storageProvider.get(this.storageKey) + : undefined; + const expired = current?.date !== this.today; + return expired + ? await this.storageProvider.put(this.storageKey, { + date: this.today, + uuids: {}, + }) + : current!; + } + + private async setStoredRatings(value: RatingStorage | undefined) { + await (value ? this.storageProvider.put(this.storageKey, value) : this.storageProvider.delete()); + } + + private async markAsRated(uid: SCUuid) { + const rating = await this.getStoredRatings(); + await this.setStoredRatings({ + ...rating, + uuids: {...rating.uuids, [uid]: true}, + }); + } + + async rate(uid: SCUuid, rating: SCRatingRequest['rating']) { + const request: SCRatingRequest = { + rating: rating, + userGroup: await this.userGroup, + uid, + }; + const response = await this.client.invokeRoute(this.ratingRoute, undefined, request); + + await this.markAsRated(uid); + + return response; + } + + async canRate(dish: SCDish): Promise { + const userGroup = ( + { + students: 'student', + employees: 'employee', + guests: 'guest', + } as Record + )[await this.userGroup]; + return dish.offers?.find(it => it.prices?.[userGroup]) !== undefined; + } + + async hasRated(uid: SCUuid): Promise { + const ratings = await this.getStoredRatings(); + return ratings.uuids[uid] ?? false; + } +} diff --git a/frontend/app/src/app/modules/data/types/dish/dish-detail-content.html b/frontend/app/src/app/modules/data/types/dish/dish-detail-content.html index ec1305bc..438b8f0c 100644 --- a/frontend/app/src/app/modules/data/types/dish/dish-detail-content.html +++ b/frontend/app/src/app/modules/data/types/dish/dish-detail-content.html @@ -72,3 +72,5 @@ [content]="'additives' | thingTranslate: item | join: ', '" > + + diff --git a/frontend/app/src/app/modules/data/types/dish/dish-detail-content.scss b/frontend/app/src/app/modules/data/types/dish/dish-detail-content.scss index 7c94e516..f55ec0a1 100644 --- a/frontend/app/src/app/modules/data/types/dish/dish-detail-content.scss +++ b/frontend/app/src/app/modules/data/types/dish/dish-detail-content.scss @@ -16,3 +16,8 @@ stapps-dish-characteristics { margin: var(--spacing-lg); margin-block-end: var(--spacing-sm); } + +stapps-rating { + position: absolute; + inset: 0 0 auto auto; +} diff --git a/frontend/app/src/assets/i18n/de.json b/frontend/app/src/assets/i18n/de.json index e4554570..2d4ca9ca 100644 --- a/frontend/app/src/assets/i18n/de.json +++ b/frontend/app/src/assets/i18n/de.json @@ -8,6 +8,9 @@ "export": "Exportieren", "share": "Teilen", "timeSuffix": "Uhr", + "ratings": { + "thank_you": "Vielen Dank für die Bewertung!" + }, "modal": { "DISMISS_NEUTRAL": "Schließen", "DISMISS_CANCEL": "Abbrechen", diff --git a/frontend/app/src/assets/i18n/en.json b/frontend/app/src/assets/i18n/en.json index 664a7e34..3e5873ee 100644 --- a/frontend/app/src/assets/i18n/en.json +++ b/frontend/app/src/assets/i18n/en.json @@ -8,6 +8,9 @@ "export": "Export", "share": "Share", "timeSuffix": "", + "ratings": { + "thank_you": "Thank you for your feedback!" + }, "modal": { "DISMISS_NEUTRAL": "Close", "DISMISS_CANCEL": "Cancel", diff --git a/frontend/app/src/assets/icons.min.woff2 b/frontend/app/src/assets/icons.min.woff2 index 81dd2c19..b7bfefc5 100644 Binary files a/frontend/app/src/assets/icons.min.woff2 and b/frontend/app/src/assets/icons.min.woff2 differ