- @if (mainImage.isSuccess()) {
- @let maybeMainImageUrl = mainImage.data();
- @if (!!maybeMainImageUrl) {
-
- } @else {
-
- }
+ @if (mainImage.isLoading()) {
+
+ } @else if (mainImage.isError()) {
+ There was an error loading the image.
+ } @else if (mainImage.isEnabled()) {
+
+ } @else {
+
}
diff --git a/src/app/shared/components/recipe-card-grid/recipe-card/recipe-card.ts b/src/app/shared/components/recipe-card-grid/recipe-card/recipe-card.ts
index 3bb13c4..f8e4086 100644
--- a/src/app/shared/components/recipe-card-grid/recipe-card/recipe-card.ts
+++ b/src/app/shared/components/recipe-card-grid/recipe-card/recipe-card.ts
@@ -1,15 +1,17 @@
import { Component, computed, inject, input } from '@angular/core';
import { Recipe } from '../../../models/Recipe.model';
import { RouterLink } from '@angular/router';
-import { injectQuery } from '@tanstack/angular-query-experimental';
+import { CreateQueryOptions, injectQuery } from '@tanstack/angular-query-experimental';
import { ImageService } from '../../../services/ImageService';
import { faGlobe, faLock, faStar, faUser } from '@fortawesome/free-solid-svg-icons';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { Logo } from '../../logo/logo';
+import { ImageViewWithBlobUrl } from '../../../client-models/ImageViewWithBlobUrl';
+import { Spinner } from '../../spinner/spinner';
@Component({
selector: 'app-recipe-card',
- imports: [RouterLink, FaIconComponent, Logo],
+ imports: [RouterLink, FaIconComponent, Logo, Spinner],
templateUrl: './recipe-card.html',
styleUrl: './recipe-card.css',
})
@@ -29,10 +31,20 @@ export class RecipeCard {
private readonly imageService = inject(ImageService);
protected readonly mainImage = injectQuery(() => {
- const recipe = this.recipe();
- return {
- queryKey: ['recipe-main-images', recipe.owner.username, recipe.slug],
- queryFn: () => this.imageService.getImage(recipe.mainImage?.url),
- };
+ const mainImageView = this.recipe().mainImage;
+ let options: Partial<
+ CreateQueryOptions
+ > = {};
+ if (mainImageView) {
+ options = this.imageService.getImage(mainImageView);
+ } else {
+ options.enabled = false;
+ }
+ return options as CreateQueryOptions<
+ ImageViewWithBlobUrl,
+ Error,
+ ImageViewWithBlobUrl,
+ [string, string, string]
+ >;
});
}
diff --git a/src/app/shared/services/ImageService.ts b/src/app/shared/services/ImageService.ts
index abe6097..7122981 100644
--- a/src/app/shared/services/ImageService.ts
+++ b/src/app/shared/services/ImageService.ts
@@ -1,10 +1,12 @@
import { inject, Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
-import { firstValueFrom, map } from 'rxjs';
+import { firstValueFrom, map, tap } from 'rxjs';
import { EndpointService } from './EndpointService';
import { ImageView } from '../models/ImageView.model';
import { SliceView } from '../models/SliceView.model';
import { QueryParams } from '../models/Query.model';
+import { QueryClient, QueryOptions, queryOptions } from '@tanstack/angular-query-experimental';
+import { ImageViewWithBlobUrl } from '../client-models/ImageViewWithBlobUrl';
@Injectable({
providedIn: 'root',
@@ -27,6 +29,7 @@ export class ImageService {
private readonly httpClient = inject(HttpClient);
private readonly endpointService = inject(EndpointService);
+ private readonly queryClient = inject(QueryClient);
public getOwnedImages(queryParams?: QueryParams): Promise> {
return firstValueFrom(
@@ -34,21 +37,32 @@ export class ImageService {
);
}
- /**
- * TODO: this api should not accept null as an input
- */
- public getImage(backendUrl?: string | null): Promise {
- if (!!backendUrl) {
- return firstValueFrom(
- this.httpClient
- .get(backendUrl, {
- responseType: 'blob',
- })
- .pipe(map((blob) => URL.createObjectURL(blob))),
- );
- } else {
- return Promise.resolve(null);
- }
+ public getImageViewWithBlobUrl(imageView: ImageView): Promise {
+ return firstValueFrom(
+ this.httpClient
+ .get(this.endpointService.getUrl('images', [imageView.owner.username, imageView.filename]), {
+ responseType: 'blob',
+ })
+ .pipe(
+ map((blob) => URL.createObjectURL(blob)),
+ map(
+ (blobUrl) =>
+ ({
+ ...imageView,
+ blobUrl,
+ }) satisfies ImageViewWithBlobUrl,
+ ),
+ ),
+ );
+ }
+
+ public getImage(
+ imageView: ImageView,
+ ): QueryOptions {
+ return queryOptions({
+ queryKey: ['image-views-with-blob-urls', imageView.owner.username, imageView.filename],
+ queryFn: () => this.getImageViewWithBlobUrl(imageView),
+ });
}
public uploadImage(
@@ -71,7 +85,15 @@ export class ImageService {
formData.append('isPublic', isPublic.toString());
}
- return firstValueFrom(this.httpClient.post(this.endpointService.getUrl('images'), formData));
+ return firstValueFrom(
+ this.httpClient.post(this.endpointService.getUrl('images'), formData).pipe(
+ tap(async () => {
+ await this.queryClient.invalidateQueries({
+ queryKey: ['image-views'],
+ });
+ }),
+ ),
+ );
}
public imageExists(username: string, filename: string): Promise {
@@ -84,7 +106,16 @@ export class ImageService {
public deleteImage(username: string, filename: string): Promise {
return firstValueFrom(
- this.httpClient.delete(this.endpointService.getUrl('images', [username, filename])),
+ this.httpClient.delete(this.endpointService.getUrl('images', [username, filename])).pipe(
+ tap(async () => {
+ await this.queryClient.refetchQueries({
+ queryKey: ['image-views', username, filename],
+ });
+ await this.queryClient.refetchQueries({
+ queryKey: ['image-views-with-blob-urls', username, filename],
+ });
+ }),
+ ),
);
}
@@ -101,7 +132,16 @@ export class ImageService {
},
): Promise {
return firstValueFrom(
- this.httpClient.put(this.endpointService.getUrl('images', [username, filename]), data),
+ this.httpClient.put(this.endpointService.getUrl('images', [username, filename]), data).pipe(
+ tap(async () => {
+ await this.queryClient.refetchQueries({
+ queryKey: ['image-views', username, filename],
+ });
+ await this.queryClient.refetchQueries({
+ queryKey: ['image-views-with-blob-urls', username, filename],
+ });
+ }),
+ ),
);
}
}