mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2026-01-04 12:32:57 +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": {
|
||||
"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",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -12,21 +12,16 @@
|
||||
* 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 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<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(
|
||||
(await globPromise(glob))
|
||||
(await glob(pattern))
|
||||
.map(file => [
|
||||
file,
|
||||
(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(
|
||||
(await globPromise(glob))
|
||||
(await glob(pattern))
|
||||
.map(file => [file, readFileSync(file, 'utf8').match(/(?<=Icon`)[\w-]+(?=`)/g) || []])
|
||||
.filter(([, values]) => values.length > 0),
|
||||
);
|
||||
|
||||
@@ -115,6 +115,7 @@ async function minifyIconFont() {
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line unicorn/prefer-top-level-await
|
||||
minifyIconFont();
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"extends": "../node_modules/@openstapps/configuration/tsconfig.json",
|
||||
"extends": "@openstapps/tsconfig",
|
||||
"compilerOptions": {
|
||||
"lib": ["es2019"]
|
||||
"module": "CommonJS",
|
||||
"moduleResolution": "Node"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
[item]="item"
|
||||
[hideThumbnail]="hideThumbnail"
|
||||
[lines]="'none'"
|
||||
[favoriteButton]="false"
|
||||
[listItemEndInteraction]="false"
|
||||
>
|
||||
<ng-template let-data>
|
||||
<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 {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,
|
||||
|
||||
@@ -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() favoriteButton = true;
|
||||
@Input() listItemEndInteraction = true;
|
||||
|
||||
@Input() lines = 'inset';
|
||||
|
||||
|
||||
@@ -32,7 +32,10 @@
|
||||
</ion-label>
|
||||
</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>
|
||||
|
||||
<ng-template #defaultContent>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
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: ', '"
|
||||
>
|
||||
</stapps-simple-card>
|
||||
|
||||
<stapps-rating [item]="item"></stapps-rating>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
"export": "Export",
|
||||
"share": "Share",
|
||||
"timeSuffix": "",
|
||||
"ratings": {
|
||||
"thank_you": "Thank you for your feedback!"
|
||||
},
|
||||
"modal": {
|
||||
"DISMISS_NEUTRAL": "Close",
|
||||
"DISMISS_CANCEL": "Cancel",
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user