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"
|
"input": "public"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"styles": ["src/material-theme.scss", "src/styles.css"]
|
"styles": ["src/material-theme.scss", "src/styles.css", "src/style-imports.scss"]
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"production": {
|
"production": {
|
||||||
|
|||||||
15
package-lock.json
generated
15
package-lock.json
generated
@ -19,7 +19,7 @@
|
|||||||
"@fortawesome/angular-fontawesome": "^4.0.0",
|
"@fortawesome/angular-fontawesome": "^4.0.0",
|
||||||
"@fortawesome/free-solid-svg-icons": "^7.1.0",
|
"@fortawesome/free-solid-svg-icons": "^7.1.0",
|
||||||
"@tanstack/angular-query-experimental": "^5.90.16",
|
"@tanstack/angular-query-experimental": "^5.90.16",
|
||||||
"ngx-sse-client": "^20.0.1",
|
"ngx-toastr": "^20.0.5",
|
||||||
"rxjs": "~7.8.0",
|
"rxjs": "~7.8.0",
|
||||||
"tslib": "^2.3.0"
|
"tslib": "^2.3.0"
|
||||||
},
|
},
|
||||||
@ -6773,17 +6773,18 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ngx-sse-client": {
|
"node_modules/ngx-toastr": {
|
||||||
"version": "20.0.1",
|
"version": "20.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/ngx-sse-client/-/ngx-sse-client-20.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/ngx-toastr/-/ngx-toastr-20.0.5.tgz",
|
||||||
"integrity": "sha512-OSFRirL5beveGj4An3lOzWwg/JZWJG4Q1TdbyW7lqSDacfwINpIjSHdWlpiQwIghKU7BtLAc6TonUGlU4MzGTQ==",
|
"integrity": "sha512-JcGu1Cbl+0SovPhxma72ygGeZHtpHWWKwBCyiabb+MSWYtXu/SOwEZ2HTWtZ4wcEYOOiy9tDQZgiEKWXpibpRw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "^2.8.1"
|
"tslib": "^2.8.1"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@angular/common": ">=20.0.0",
|
"@angular/common": "^21.0.0",
|
||||||
"@angular/core": ">=20.0.0"
|
"@angular/core": "^21.0.0",
|
||||||
|
"rxjs": "^7.8.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/node-addon-api": {
|
"node_modules/node-addon-api": {
|
||||||
|
|||||||
@ -33,6 +33,7 @@
|
|||||||
"@fortawesome/angular-fontawesome": "^4.0.0",
|
"@fortawesome/angular-fontawesome": "^4.0.0",
|
||||||
"@fortawesome/free-solid-svg-icons": "^7.1.0",
|
"@fortawesome/free-solid-svg-icons": "^7.1.0",
|
||||||
"@tanstack/angular-query-experimental": "^5.90.16",
|
"@tanstack/angular-query-experimental": "^5.90.16",
|
||||||
|
"ngx-toastr": "^20.0.5",
|
||||||
"rxjs": "~7.8.0",
|
"rxjs": "~7.8.0",
|
||||||
"tslib": "^2.3.0"
|
"tslib": "^2.3.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core';
|
import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core';
|
||||||
import { provideRouter } from '@angular/router';
|
import { provideRouter } from '@angular/router';
|
||||||
|
|
||||||
import { routes } from './app.routes';
|
import { routes } from './app.routes';
|
||||||
import { provideHttpClient, withInterceptors } from '@angular/common/http';
|
import { provideHttpClient, withInterceptors } from '@angular/common/http';
|
||||||
import { authInterceptor } from './shared/interceptors/auth.interceptor';
|
import { authInterceptor } from './shared/interceptors/auth.interceptor';
|
||||||
import { provideTanStackQuery, QueryClient } from '@tanstack/angular-query-experimental';
|
import { provideTanStackQuery, QueryClient } from '@tanstack/angular-query-experimental';
|
||||||
import { withDevtools } from '@tanstack/angular-query-experimental/devtools';
|
import { withDevtools } from '@tanstack/angular-query-experimental/devtools';
|
||||||
|
import { provideToastr } from 'ngx-toastr';
|
||||||
|
|
||||||
export const appConfig: ApplicationConfig = {
|
export const appConfig: ApplicationConfig = {
|
||||||
providers: [
|
providers: [
|
||||||
@ -22,5 +22,6 @@ export const appConfig: ApplicationConfig = {
|
|||||||
}),
|
}),
|
||||||
withDevtools(),
|
withDevtools(),
|
||||||
),
|
),
|
||||||
|
provideToastr(),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@ -26,3 +26,13 @@ article {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
object-fit: cover;
|
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 id="recipe-header">
|
||||||
<div>
|
<div>
|
||||||
<h1>{{ recipe.title }}</h1>
|
<h1>{{ recipe.title }}</h1>
|
||||||
@if (isLoggedIn()) {
|
<div class="recipe-actions">
|
||||||
<button id="star" matButton="filled" (click)="starMutation.mutate()">
|
@if (isLoggedIn()) {
|
||||||
<div id="star-label">
|
<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" />
|
<fa-icon [icon]="faStar" />
|
||||||
<span>Star</span>
|
|
||||||
<span id="star-count">{{ recipe.starCount }}</span>
|
<span id="star-count">{{ recipe.starCount }}</span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
}
|
||||||
} @else {
|
@if (isOwner()) {
|
||||||
<div>
|
<button class="actions-button" matButton="text" [matMenuTriggerFor]="recipeActionsMenu">
|
||||||
<fa-icon [icon]="faStar" />
|
<fa-icon [icon]="faEllipsis" size="3x"></fa-icon>
|
||||||
<span id="star-count">{{ recipe.starCount }}</span>
|
</button>
|
||||||
</div>
|
<mat-menu #recipeActionsMenu="matMenu">
|
||||||
}
|
<button mat-menu-item (click)="onRecipeDelete()">Delete recipe</button>
|
||||||
|
</mat-menu>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { Component, computed, inject, input } from '@angular/core';
|
|||||||
import { RecipeView } from '../../../shared/models/Recipe.model';
|
import { RecipeView } from '../../../shared/models/Recipe.model';
|
||||||
import { CreateQueryOptions, injectMutation, injectQuery } from '@tanstack/angular-query-experimental';
|
import { CreateQueryOptions, injectMutation, injectQuery } from '@tanstack/angular-query-experimental';
|
||||||
import { ImageService } from '../../../shared/services/ImageService';
|
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 { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||||
import { RecipeService } from '../../../shared/services/RecipeService';
|
import { RecipeService } from '../../../shared/services/RecipeService';
|
||||||
import { AuthService } from '../../../shared/services/AuthService';
|
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 { MatButton } from '@angular/material/button';
|
||||||
import { Spinner } from '../../../shared/components/spinner/spinner';
|
import { Spinner } from '../../../shared/components/spinner/spinner';
|
||||||
import { ImageViewWithBlobUrl } from '../../../shared/client-models/ImageViewWithBlobUrl';
|
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({
|
@Component({
|
||||||
selector: 'app-recipe-page-content',
|
selector: 'app-recipe-page-content',
|
||||||
imports: [FaIconComponent, RecipeCommentsList, MatButton, Spinner],
|
imports: [FaIconComponent, RecipeCommentsList, MatButton, Spinner, MatMenuTrigger, MatMenu, MatMenuItem],
|
||||||
templateUrl: './recipe-page-content.html',
|
templateUrl: './recipe-page-content.html',
|
||||||
styleUrl: './recipe-page-content.css',
|
styleUrl: './recipe-page-content.css',
|
||||||
})
|
})
|
||||||
@ -23,8 +29,10 @@ export class RecipePageContent {
|
|||||||
private readonly imageService = inject(ImageService);
|
private readonly imageService = inject(ImageService);
|
||||||
private readonly recipeService = inject(RecipeService);
|
private readonly recipeService = inject(RecipeService);
|
||||||
private readonly authService = inject(AuthService);
|
private readonly authService = inject(AuthService);
|
||||||
|
private readonly router = inject(Router);
|
||||||
|
|
||||||
protected readonly isLoggedIn = computed(() => !!this.authService.accessToken());
|
protected readonly isLoggedIn = computed(() => !!this.authService.accessToken());
|
||||||
|
protected readonly isOwner = computed(() => !!this.recipeView().isOwner);
|
||||||
|
|
||||||
protected readonly mainImageQuery = injectQuery(() => {
|
protected readonly mainImageQuery = injectQuery(() => {
|
||||||
let options: Partial<
|
let options: Partial<
|
||||||
@ -48,8 +56,30 @@ export class RecipePageContent {
|
|||||||
mutationFn: () => this.recipeService.toggleStar(this.recipeView()),
|
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 faStar = faStar;
|
||||||
protected readonly faUser = faUser;
|
protected readonly faUser = faUser;
|
||||||
protected readonly faGlobe = faGlobe;
|
protected readonly faGlobe = faGlobe;
|
||||||
protected readonly faLock = faLock;
|
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 { inject, Injectable } from '@angular/core';
|
||||||
import { HttpClient } from '@angular/common/http';
|
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 { Recipe, RecipeInfoViews, RecipeView } from '../models/Recipe.model';
|
||||||
import { AuthService } from './AuthService';
|
import { AuthService } from './AuthService';
|
||||||
import { QueryClient } from '@tanstack/angular-query-experimental';
|
import { QueryClient } from '@tanstack/angular-query-experimental';
|
||||||
@ -78,4 +78,8 @@ export class RecipeService {
|
|||||||
);
|
);
|
||||||
return recipeInfoViews.results;
|
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