MME-13 Add delete menu action and user feedback for recipes.
This commit is contained in:
parent
51bdbbbb6a
commit
09fb9a3d98
@ -25,7 +25,7 @@
|
||||
"input": "public"
|
||||
}
|
||||
],
|
||||
"styles": ["src/material-theme.scss", "src/styles.css"]
|
||||
"styles": ["src/material-theme.scss", "src/styles.css", "src/style-imports.scss"]
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
|
||||
15
package-lock.json
generated
15
package-lock.json
generated
@ -19,7 +19,7 @@
|
||||
"@fortawesome/angular-fontawesome": "^4.0.0",
|
||||
"@fortawesome/free-solid-svg-icons": "^7.1.0",
|
||||
"@tanstack/angular-query-experimental": "^5.90.16",
|
||||
"ngx-sse-client": "^20.0.1",
|
||||
"ngx-toastr": "^20.0.5",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
@ -6773,17 +6773,18 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/ngx-sse-client": {
|
||||
"version": "20.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ngx-sse-client/-/ngx-sse-client-20.0.1.tgz",
|
||||
"integrity": "sha512-OSFRirL5beveGj4An3lOzWwg/JZWJG4Q1TdbyW7lqSDacfwINpIjSHdWlpiQwIghKU7BtLAc6TonUGlU4MzGTQ==",
|
||||
"node_modules/ngx-toastr": {
|
||||
"version": "20.0.5",
|
||||
"resolved": "https://registry.npmjs.org/ngx-toastr/-/ngx-toastr-20.0.5.tgz",
|
||||
"integrity": "sha512-JcGu1Cbl+0SovPhxma72ygGeZHtpHWWKwBCyiabb+MSWYtXu/SOwEZ2HTWtZ4wcEYOOiy9tDQZgiEKWXpibpRw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/common": ">=20.0.0",
|
||||
"@angular/core": ">=20.0.0"
|
||||
"@angular/common": "^21.0.0",
|
||||
"@angular/core": "^21.0.0",
|
||||
"rxjs": "^7.8.2"
|
||||
}
|
||||
},
|
||||
"node_modules/node-addon-api": {
|
||||
|
||||
@ -33,6 +33,7 @@
|
||||
"@fortawesome/angular-fontawesome": "^4.0.0",
|
||||
"@fortawesome/free-solid-svg-icons": "^7.1.0",
|
||||
"@tanstack/angular-query-experimental": "^5.90.16",
|
||||
"ngx-toastr": "^20.0.5",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core';
|
||||
import { provideRouter } from '@angular/router';
|
||||
|
||||
import { routes } from './app.routes';
|
||||
import { provideHttpClient, withInterceptors } from '@angular/common/http';
|
||||
import { authInterceptor } from './shared/interceptors/auth.interceptor';
|
||||
import { provideTanStackQuery, QueryClient } from '@tanstack/angular-query-experimental';
|
||||
import { withDevtools } from '@tanstack/angular-query-experimental/devtools';
|
||||
import { provideToastr } from 'ngx-toastr';
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [
|
||||
@ -22,5 +22,6 @@ export const appConfig: ApplicationConfig = {
|
||||
}),
|
||||
withDevtools(),
|
||||
),
|
||||
provideToastr(),
|
||||
],
|
||||
};
|
||||
|
||||
@ -26,3 +26,13 @@ article {
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.recipe-actions {
|
||||
display: flex;
|
||||
column-gap: 10px;
|
||||
}
|
||||
|
||||
.actions-button {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@ -3,20 +3,30 @@
|
||||
<div id="recipe-header">
|
||||
<div>
|
||||
<h1>{{ recipe.title }}</h1>
|
||||
@if (isLoggedIn()) {
|
||||
<button id="star" matButton="filled" (click)="starMutation.mutate()">
|
||||
<div id="star-label">
|
||||
<div class="recipe-actions">
|
||||
@if (isLoggedIn()) {
|
||||
<button id="star" matButton="filled" (click)="starMutation.mutate()">
|
||||
<div id="star-label">
|
||||
<fa-icon [icon]="faStar" />
|
||||
<span>Star</span>
|
||||
<span id="star-count">{{ recipe.starCount }}</span>
|
||||
</div>
|
||||
</button>
|
||||
} @else {
|
||||
<div>
|
||||
<fa-icon [icon]="faStar" />
|
||||
<span>Star</span>
|
||||
<span id="star-count">{{ recipe.starCount }}</span>
|
||||
</div>
|
||||
</button>
|
||||
} @else {
|
||||
<div>
|
||||
<fa-icon [icon]="faStar" />
|
||||
<span id="star-count">{{ recipe.starCount }}</span>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@if (isOwner()) {
|
||||
<button class="actions-button" matButton="text" [matMenuTriggerFor]="recipeActionsMenu">
|
||||
<fa-icon [icon]="faEllipsis" size="3x"></fa-icon>
|
||||
</button>
|
||||
<mat-menu #recipeActionsMenu="matMenu">
|
||||
<button mat-menu-item (click)="onRecipeDelete()">Delete recipe</button>
|
||||
</mat-menu>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div>
|
||||
|
||||
@ -2,7 +2,7 @@ import { Component, computed, inject, input } from '@angular/core';
|
||||
import { RecipeView } from '../../../shared/models/Recipe.model';
|
||||
import { CreateQueryOptions, injectMutation, injectQuery } from '@tanstack/angular-query-experimental';
|
||||
import { ImageService } from '../../../shared/services/ImageService';
|
||||
import { faGlobe, faLock, faStar, faUser } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faEllipsis, faGlobe, faLock, faStar, faUser } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import { RecipeService } from '../../../shared/services/RecipeService';
|
||||
import { AuthService } from '../../../shared/services/AuthService';
|
||||
@ -10,10 +10,16 @@ import { RecipeCommentsList } from '../../../shared/components/recipe-comments-l
|
||||
import { MatButton } from '@angular/material/button';
|
||||
import { Spinner } from '../../../shared/components/spinner/spinner';
|
||||
import { ImageViewWithBlobUrl } from '../../../shared/client-models/ImageViewWithBlobUrl';
|
||||
import { MatMenu, MatMenuItem, MatMenuTrigger } from '@angular/material/menu';
|
||||
import { Router } from '@angular/router';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { ConfirmationDialog } from '../../../shared/components/confirmation-dialog/confirmation-dialog';
|
||||
import { ConfirmationDialogData } from '../../../shared/components/confirmation-dialog/ConfirmationDialogData';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
|
||||
@Component({
|
||||
selector: 'app-recipe-page-content',
|
||||
imports: [FaIconComponent, RecipeCommentsList, MatButton, Spinner],
|
||||
imports: [FaIconComponent, RecipeCommentsList, MatButton, Spinner, MatMenuTrigger, MatMenu, MatMenuItem],
|
||||
templateUrl: './recipe-page-content.html',
|
||||
styleUrl: './recipe-page-content.css',
|
||||
})
|
||||
@ -23,8 +29,10 @@ export class RecipePageContent {
|
||||
private readonly imageService = inject(ImageService);
|
||||
private readonly recipeService = inject(RecipeService);
|
||||
private readonly authService = inject(AuthService);
|
||||
private readonly router = inject(Router);
|
||||
|
||||
protected readonly isLoggedIn = computed(() => !!this.authService.accessToken());
|
||||
protected readonly isOwner = computed(() => !!this.recipeView().isOwner);
|
||||
|
||||
protected readonly mainImageQuery = injectQuery(() => {
|
||||
let options: Partial<
|
||||
@ -48,8 +56,30 @@ export class RecipePageContent {
|
||||
mutationFn: () => this.recipeService.toggleStar(this.recipeView()),
|
||||
}));
|
||||
|
||||
private readonly dialog = inject(MatDialog);
|
||||
private readonly toastrService = inject(ToastrService);
|
||||
|
||||
protected onRecipeDelete(): void {
|
||||
const dialogRef = this.dialog.open<ConfirmationDialog, ConfirmationDialogData, boolean>(ConfirmationDialog, {
|
||||
data: {
|
||||
title: 'Delete Recipe',
|
||||
message: 'Are you sure you wish to delete this recipe?',
|
||||
},
|
||||
});
|
||||
dialogRef.afterClosed().subscribe((confirmed) => {
|
||||
if (confirmed) {
|
||||
const recipe = this.recipeView().recipe;
|
||||
this.recipeService.deleteRecipe(recipe.owner.username, recipe.slug).subscribe(async () => {
|
||||
this.toastrService.success('Recipe successfully deleted');
|
||||
await this.router.navigate(['/']);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected readonly faStar = faStar;
|
||||
protected readonly faUser = faUser;
|
||||
protected readonly faGlobe = faGlobe;
|
||||
protected readonly faLock = faLock;
|
||||
protected readonly faEllipsis = faEllipsis;
|
||||
}
|
||||
|
||||
@ -0,0 +1,4 @@
|
||||
export interface ConfirmationDialogData {
|
||||
title: string;
|
||||
message: string;
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
.confirmation-actions {
|
||||
display: flex;
|
||||
column-gap: 10px;
|
||||
}
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
<app-dialog-container [title]="data.title">
|
||||
<p>{{ data.message }}</p>
|
||||
<div class="confirmation-actions">
|
||||
<button matButton="outlined" (click)="onCancel()">Cancel</button>
|
||||
<button matButton="filled" (click)="onOk()">Ok</button>
|
||||
</div>
|
||||
</app-dialog-container>
|
||||
@ -0,0 +1,22 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ConfirmationDialog } from './confirmation-dialog';
|
||||
|
||||
describe('ConfirmationDialog', () => {
|
||||
let component: ConfirmationDialog;
|
||||
let fixture: ComponentFixture<ConfirmationDialog>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ConfirmationDialog],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ConfirmationDialog);
|
||||
component = fixture.componentInstance;
|
||||
await fixture.whenStable();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,25 @@
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { DialogContainer } from '../dialog-container/dialog-container';
|
||||
import { MatButton } from '@angular/material/button';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { ConfirmationDialogData } from './ConfirmationDialogData';
|
||||
|
||||
@Component({
|
||||
selector: 'app-confirmation-dialog',
|
||||
imports: [DialogContainer, MatButton],
|
||||
templateUrl: './confirmation-dialog.html',
|
||||
styleUrl: './confirmation-dialog.css',
|
||||
})
|
||||
export class ConfirmationDialog {
|
||||
protected readonly data: ConfirmationDialogData = inject(MAT_DIALOG_DATA);
|
||||
|
||||
private readonly dialogRef: MatDialogRef<ConfirmationDialogData, boolean> = inject(MatDialogRef);
|
||||
|
||||
protected onCancel(): void {
|
||||
this.dialogRef.close(false);
|
||||
}
|
||||
|
||||
protected onOk(): void {
|
||||
this.dialogRef.close(true);
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { firstValueFrom, lastValueFrom, map } from 'rxjs';
|
||||
import { firstValueFrom, lastValueFrom, map, Observable } from 'rxjs';
|
||||
import { Recipe, RecipeInfoViews, RecipeView } from '../models/Recipe.model';
|
||||
import { AuthService } from './AuthService';
|
||||
import { QueryClient } from '@tanstack/angular-query-experimental';
|
||||
@ -78,4 +78,8 @@ export class RecipeService {
|
||||
);
|
||||
return recipeInfoViews.results;
|
||||
}
|
||||
|
||||
public deleteRecipe(username: string, slug: string): Observable<void> {
|
||||
return this.http.delete<void>(this.endpointService.getUrl('recipes', [username, slug]));
|
||||
}
|
||||
}
|
||||
|
||||
1
src/style-imports.scss
Normal file
1
src/style-imports.scss
Normal file
@ -0,0 +1 @@
|
||||
@use "ngx-toastr/toastr";
|
||||
Loading…
Reference in New Issue
Block a user