mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2026-01-22 09:32:41 +00:00
feat: rating for dishes
This commit is contained in:
67
frontend/app/cypress/integration/rating.spec.ts
Normal file
67
frontend/app/cypress/integration/rating.spec.ts
Normal file
@@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -15,13 +15,13 @@
|
|||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"analyze": "webpack-bundle-analyzer www/stats.json",
|
"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:analyze": "npm run build:stats && npm run analyze",
|
||||||
"build:android": "ionic capacitor build android --no-open && cd android && ./gradlew clean assembleDebug && cd ..",
|
"build:android": "ionic capacitor build android --no-open && cd android && ./gradlew clean assembleDebug && cd ..",
|
||||||
"build:prod": "ng build --configuration=production",
|
"build:prod": "ng build --configuration=production",
|
||||||
"build:stats": "ng build --configuration=production --stats-json",
|
"build:stats": "ng build --configuration=production --stats-json",
|
||||||
"changelog": "conventional-changelog -p angular -i src/assets/about/CHANGELOG.md -s -r 0",
|
"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:open": "cypress open",
|
||||||
"cypress:run": "cypress run",
|
"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\"",
|
"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",
|
"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": "ng lint",
|
||||||
"lint:fix": "eslint --fix -c .eslintrc.json --ignore-path .eslintignore --ext .ts,.html src/",
|
"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",
|
"ng": "ng",
|
||||||
"postinstall": "(jetify && node ngcc-postinstall.mjs) || echo \"skipping jetify in production mode\"",
|
"postinstall": "(jetify && node ngcc-postinstall.mjs) || echo \"skipping jetify in production mode\"",
|
||||||
"preview": "http-server www --p 8101 -o",
|
"preview": "http-server www --p 8101 -o",
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ const modifiedFont = fontkit.openSync(config.outputPath);
|
|||||||
|
|
||||||
let success = true;
|
let success = true;
|
||||||
|
|
||||||
|
// eslint-disable-next-line unicorn/prefer-top-level-await
|
||||||
checkAll().then(() => {
|
checkAll().then(() => {
|
||||||
console.log();
|
console.log();
|
||||||
if (success) {
|
if (success) {
|
||||||
|
|||||||
@@ -12,21 +12,16 @@
|
|||||||
* You should have received a copy of the GNU General Public License along with
|
* You should have received a copy of the GNU General Public License along with
|
||||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
import glob from 'glob';
|
import {glob} from 'glob';
|
||||||
import {readFileSync} from 'fs';
|
import {readFileSync} from 'fs';
|
||||||
import {matchPropertyContent, matchTagProperties} from '../src/app/util/ion-icon/icon-match';
|
import {matchPropertyContent, matchTagProperties} from '../src/app/util/ion-icon/icon-match';
|
||||||
|
|
||||||
const globPromise = (pattern: string) =>
|
|
||||||
new Promise<string[]>((resolve, reject) =>
|
|
||||||
glob(pattern, (error, files) => (error ? reject(error) : resolve(files))),
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export async function getUsedIconsHtml(glob = 'src/**/*.html'): Promise<Record<string, string[]>> {
|
export async function getUsedIconsHtml(pattern = 'src/**/*.html'): Promise<Record<string, string[]>> {
|
||||||
return Object.fromEntries(
|
return Object.fromEntries(
|
||||||
(await globPromise(glob))
|
(await glob(pattern))
|
||||||
.map(file => [
|
.map(file => [
|
||||||
file,
|
file,
|
||||||
(readFileSync(file, 'utf8')
|
(readFileSync(file, 'utf8')
|
||||||
@@ -43,9 +38,9 @@ export async function getUsedIconsHtml(glob = 'src/**/*.html'): Promise<Record<s
|
|||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export async function getUsedIconsTS(glob = 'src/**/*.ts'): Promise<Record<string, string[]>> {
|
export async function getUsedIconsTS(pattern = 'src/**/*.ts'): Promise<Record<string, string[]>> {
|
||||||
return Object.fromEntries(
|
return Object.fromEntries(
|
||||||
(await globPromise(glob))
|
(await glob(pattern))
|
||||||
.map(file => [file, readFileSync(file, 'utf8').match(/(?<=Icon`)[\w-]+(?=`)/g) || []])
|
.map(file => [file, readFileSync(file, 'utf8').match(/(?<=Icon`)[\w-]+(?=`)/g) || []])
|
||||||
.filter(([, values]) => values.length > 0),
|
.filter(([, values]) => values.length > 0),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -115,6 +115,7 @@ async function minifyIconFont() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line unicorn/prefer-top-level-await
|
||||||
minifyIconFont();
|
minifyIconFont();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"extends": "../node_modules/@openstapps/configuration/tsconfig.json",
|
"extends": "@openstapps/tsconfig",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"lib": ["es2019"]
|
"module": "CommonJS",
|
||||||
|
"moduleResolution": "Node"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
<!--
|
<!--
|
||||||
~ Copyright (C) 2022 StApps
|
~ Copyright (C) 2022 StApps
|
||||||
~ This program is free software: you can redistribute it and/or modify it
|
~ 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
|
~ under the terms of the GNU General Public License as published by the Free
|
||||||
~ Software Foundation, version 3.
|
~ Software Foundation, version 3.
|
||||||
~
|
~
|
||||||
~ This program is distributed in the hope that it will be useful, but WITHOUT
|
~ This program is distributed in the hope that it will be useful, but WITHOUT
|
||||||
~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||||
~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
~ more details.
|
~ more details.
|
||||||
~
|
~
|
||||||
~ You should have received a copy of the GNU General Public License along with
|
~ You should have received a copy of the GNU General Public License along with
|
||||||
~ this program. If not, see <https://www.gnu.org/licenses/>.
|
~ this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<stapps-data-list-item
|
<stapps-data-list-item
|
||||||
[item]="item"
|
[item]="item"
|
||||||
[hideThumbnail]="hideThumbnail"
|
[hideThumbnail]="hideThumbnail"
|
||||||
[lines]="'none'"
|
[lines]="'none'"
|
||||||
[favoriteButton]="false"
|
[listItemEndInteraction]="false"
|
||||||
>
|
>
|
||||||
<ng-template let-data>
|
<ng-template let-data>
|
||||||
<stapps-assessment-list-item [item]="data"></stapps-assessment-list-item>
|
<stapps-assessment-list-item [item]="data"></stapps-assessment-list-item>
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ import {PeriodicalDetailContentComponent} from './types/periodical/periodical-de
|
|||||||
import {DataListItemHostDirective} from './list/data-list-item-host.directive';
|
import {DataListItemHostDirective} from './list/data-list-item-host.directive';
|
||||||
import {DataListItemHostDefaultComponent} from './list/data-list-item-host-default.component';
|
import {DataListItemHostDefaultComponent} from './list/data-list-item-host-default.component';
|
||||||
import {browserFactory, SimpleBrowser} from '../../util/browser.factory';
|
import {browserFactory, SimpleBrowser} from '../../util/browser.factory';
|
||||||
|
import {StappsRatingComponent} from './elements/rating.component';
|
||||||
import {DishCharacteristicsComponent} from './types/dish/dish-characteristics.component';
|
import {DishCharacteristicsComponent} from './types/dish/dish-characteristics.component';
|
||||||
import {SkeletonListComponent} from './list/skeleton-list.component';
|
import {SkeletonListComponent} from './list/skeleton-list.component';
|
||||||
|
|
||||||
@@ -155,6 +156,7 @@ import {SkeletonListComponent} from './list/skeleton-list.component';
|
|||||||
SimpleCardComponent,
|
SimpleCardComponent,
|
||||||
SkeletonListItemComponent,
|
SkeletonListItemComponent,
|
||||||
SkeletonSegmentComponent,
|
SkeletonSegmentComponent,
|
||||||
|
StappsRatingComponent,
|
||||||
SkeletonSimpleCardComponent,
|
SkeletonSimpleCardComponent,
|
||||||
TreeListComponent,
|
TreeListComponent,
|
||||||
TreeListFragmentComponent,
|
TreeListFragmentComponent,
|
||||||
@@ -170,7 +172,6 @@ import {SkeletonListComponent} from './list/skeleton-list.component';
|
|||||||
PeriodicalListItemComponent,
|
PeriodicalListItemComponent,
|
||||||
PeriodicalDetailContentComponent,
|
PeriodicalDetailContentComponent,
|
||||||
],
|
],
|
||||||
entryComponents: [DataListComponent, SimpleDataListComponent],
|
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
DataRoutingModule,
|
DataRoutingModule,
|
||||||
|
|||||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
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,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
36
frontend/app/src/app/modules/data/elements/rating.html
Normal file
36
frontend/app/src/app/modules/data/elements/rating.html
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<!--
|
||||||
|
~ 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 <https://www.gnu.org/licenses/>.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<ion-button
|
||||||
|
*ngIf="canBeRated"
|
||||||
|
fill="clear"
|
||||||
|
(click)="$event.stopPropagation(); rate = true"
|
||||||
|
[disabled]="rated"
|
||||||
|
>
|
||||||
|
<ion-icon slot="icon-only" color="medium" name="thumbs_up_down"></ion-icon>
|
||||||
|
</ion-button>
|
||||||
|
|
||||||
|
<div class="rating-stars" *ngIf="rate && !rated" [@rating]="rating ? 'rated' : 'abandoned'">
|
||||||
|
<ion-icon
|
||||||
|
[class.rated-value]="rating === i"
|
||||||
|
*ngFor="let i of [5, 4, 3, 2, 1]"
|
||||||
|
(click)="$event.stopPropagation(); submitRating(i)"
|
||||||
|
slot="icon-only"
|
||||||
|
size="32"
|
||||||
|
color="medium"
|
||||||
|
name="grade"
|
||||||
|
></ion-icon>
|
||||||
|
<label class="thank-you">{{ 'ratings.thank_you' | translate }}</label>
|
||||||
|
</div>
|
||||||
66
frontend/app/src/app/modules/data/elements/rating.scss
Normal file
66
frontend/app/src/app/modules/data/elements/rating.scss
Normal file
@@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -36,7 +36,7 @@ export class DataListItemComponent {
|
|||||||
*/
|
*/
|
||||||
@Input() item: SCThings;
|
@Input() item: SCThings;
|
||||||
|
|
||||||
@Input() favoriteButton = true;
|
@Input() listItemEndInteraction = true;
|
||||||
|
|
||||||
@Input() lines = 'inset';
|
@Input() lines = 'inset';
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,10 @@
|
|||||||
</ion-label>
|
</ion-label>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<stapps-favorite-button *ngIf="favoriteButton" [item]="$any(item)"></stapps-favorite-button>
|
<ng-container *ngIf="listItemEndInteraction" [ngSwitch]="item.type">
|
||||||
|
<stapps-rating *ngSwitchCase="'dish'" [item]="$any(item)"></stapps-rating>
|
||||||
|
<stapps-favorite-button *ngSwitchDefault [item]="$any(item)"></stapps-favorite-button>
|
||||||
|
</ng-container>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
|
||||||
<ng-template #defaultContent>
|
<ng-template #defaultContent>
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ ion-item {
|
|||||||
@include border-radius-in-parallax(var(--border-radius-default));
|
@include border-radius-in-parallax(var(--border-radius-default));
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
--inner-padding-end: 0;
|
--inner-padding-end: 0;
|
||||||
--padding-start: var(--spacing-sm);
|
--padding-start: 0;
|
||||||
margin: var(--spacing-sm);
|
margin: var(--spacing-sm);
|
||||||
|
|
||||||
ion-thumbnail {
|
ion-thumbnail {
|
||||||
@@ -44,6 +44,7 @@ ion-item {
|
|||||||
ion-label {
|
ion-label {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
|
padding-left: var(--spacing-sm);
|
||||||
|
|
||||||
div {
|
div {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -68,11 +69,12 @@ ion-item {
|
|||||||
flex-basis: min-content;
|
flex-basis: min-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stapps-long-inline-text,
|
||||||
.title {
|
.title {
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
white-space: break-spaces;
|
white-space: break-spaces;
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
-webkit-line-clamp: 3;
|
-webkit-line-clamp: 2;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,11 +82,33 @@ ion-item {
|
|||||||
display: none;
|
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
|
// fix for Safari
|
||||||
stapps-offers-in-list {
|
stapps-offers-in-list {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
right: 0;
|
left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
stapps-offers-in-list .place {
|
stapps-offers-in-list .place {
|
||||||
|
|||||||
125
frontend/app/src/app/modules/data/rating.provider.ts
Normal file
125
frontend/app/src/app/modules/data/rating.provider.ts
Normal file
@@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
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<SCUuid, true>;
|
||||||
|
}
|
||||||
|
|
||||||
|
@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<SCUserGroup> {
|
||||||
|
return this.settingsProvider
|
||||||
|
.getSetting('profile', 'group')
|
||||||
|
.then(it => (it as SCUserGroupSetting).value as SCUserGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getStoredRatings(): Promise<RatingStorage> {
|
||||||
|
const has = await this.storageProvider.has(this.storageKey);
|
||||||
|
const current = has
|
||||||
|
? await this.storageProvider.get<RatingStorage | undefined>(this.storageKey)
|
||||||
|
: undefined;
|
||||||
|
const expired = current?.date !== this.today;
|
||||||
|
return expired
|
||||||
|
? await this.storageProvider.put<RatingStorage>(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<SCRatingResponse>(this.ratingRoute, undefined, request);
|
||||||
|
|
||||||
|
await this.markAsRated(uid);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
async canRate(dish: SCDish): Promise<boolean> {
|
||||||
|
const userGroup = (
|
||||||
|
{
|
||||||
|
students: 'student',
|
||||||
|
employees: 'employee',
|
||||||
|
guests: 'guest',
|
||||||
|
} as Record<SCUserGroup, keyof SCAcademicPriceGroup>
|
||||||
|
)[await this.userGroup];
|
||||||
|
return dish.offers?.find(it => it.prices?.[userGroup]) !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
async hasRated(uid: SCUuid): Promise<boolean> {
|
||||||
|
const ratings = await this.getStoredRatings();
|
||||||
|
return ratings.uuids[uid] ?? false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -72,3 +72,5 @@
|
|||||||
[content]="'additives' | thingTranslate: item | join: ', '"
|
[content]="'additives' | thingTranslate: item | join: ', '"
|
||||||
>
|
>
|
||||||
</stapps-simple-card>
|
</stapps-simple-card>
|
||||||
|
|
||||||
|
<stapps-rating [item]="item"></stapps-rating>
|
||||||
|
|||||||
@@ -16,3 +16,8 @@ stapps-dish-characteristics {
|
|||||||
margin: var(--spacing-lg);
|
margin: var(--spacing-lg);
|
||||||
margin-block-end: var(--spacing-sm);
|
margin-block-end: var(--spacing-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stapps-rating {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0 0 auto auto;
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,9 @@
|
|||||||
"export": "Exportieren",
|
"export": "Exportieren",
|
||||||
"share": "Teilen",
|
"share": "Teilen",
|
||||||
"timeSuffix": "Uhr",
|
"timeSuffix": "Uhr",
|
||||||
|
"ratings": {
|
||||||
|
"thank_you": "Vielen Dank für die Bewertung!"
|
||||||
|
},
|
||||||
"modal": {
|
"modal": {
|
||||||
"DISMISS_NEUTRAL": "Schließen",
|
"DISMISS_NEUTRAL": "Schließen",
|
||||||
"DISMISS_CANCEL": "Abbrechen",
|
"DISMISS_CANCEL": "Abbrechen",
|
||||||
|
|||||||
@@ -8,6 +8,9 @@
|
|||||||
"export": "Export",
|
"export": "Export",
|
||||||
"share": "Share",
|
"share": "Share",
|
||||||
"timeSuffix": "",
|
"timeSuffix": "",
|
||||||
|
"ratings": {
|
||||||
|
"thank_you": "Thank you for your feedback!"
|
||||||
|
},
|
||||||
"modal": {
|
"modal": {
|
||||||
"DISMISS_NEUTRAL": "Close",
|
"DISMISS_NEUTRAL": "Close",
|
||||||
"DISMISS_CANCEL": "Cancel",
|
"DISMISS_CANCEL": "Cancel",
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user