MME-8 Main image selected and passed to backend.

This commit is contained in:
Jesse Brault 2026-02-06 13:17:22 -06:00
parent b07a8fd90b
commit 9b90e1033e
11 changed files with 76 additions and 35 deletions

View File

@ -1,3 +1,5 @@
import { ImageView } from '../../../../shared/models/ImageView.model';
export interface EnterRecipeDataSubmitEvent { export interface EnterRecipeDataSubmitEvent {
title: string; title: string;
slug: string; slug: string;
@ -6,5 +8,6 @@ export interface EnterRecipeDataSubmitEvent {
name: string; name: string;
notes?: string | null; notes?: string | null;
}>; }>;
mainImage: ImageView | null;
rawText: string; rawText: string;
} }

View File

@ -88,7 +88,10 @@
<button matButton="outlined" (click)="openImageUploadDialog()" type="button">Upload Image</button> <button matButton="outlined" (click)="openImageUploadDialog()" type="button">Upload Image</button>
<h4>Select Main Image</h4> <h4>Select Main Image</h4>
<app-image-select (select)="onMainImageSelect($event)" [selectedUsernameFilename]="mainImageUsernameFilename()"></app-image-select> <app-image-select
(select)="onMainImageSelect($event)"
[selectedUsernameFilename]="mainImageUsernameFilename()"
></app-image-select>
<h3>Recipe Text</h3> <h3>Recipe Text</h3>
<mat-form-field> <mat-form-field>

View File

@ -211,6 +211,7 @@ export class EnterRecipeData implements OnInit {
title: value.title!, title: value.title!,
slug: value.slug!, slug: value.slug!,
ingredients: this.ingredientModels().map((ingredientModel) => ingredientModel.draft), ingredients: this.ingredientModels().map((ingredientModel) => ingredientModel.draft),
mainImage: this.mainImage(),
rawText: value.text!, rawText: value.text!,
}); });
} }

View File

@ -4,14 +4,12 @@
gap: 5px; gap: 5px;
} }
.image-grid > img { .image-grid-image {
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: cover; object-fit: cover;
} }
.selected-image { mat-card-content p {
border: 5px solid var(--primary-black); overflow-wrap: anywhere;
border-radius: 10px;
box-sizing: border-box;
} }

View File

@ -11,14 +11,31 @@
<p>There was an error loading this image.</p> <p>There was an error loading this image.</p>
} @else { } @else {
@let imageData = imageQuery.data(); @let imageData = imageQuery.data();
<mat-card>
<img <img
mat-card-image
[src]="imageData!.blobUrl" [src]="imageData!.blobUrl"
alt="imageData!.imageView.alt" alt="imageData!.imageView.alt"
(click)="onImageClick(imageData!.imageView)" (click)="onImageClick(imageData!.imageView)"
[ngClass]="{ 'selected-image': isSelected(imageData!.imageView) }" class="image-grid-image"
> />
<mat-card-content>
<p>{{ imageData!.imageView.filename }}</p>
</mat-card-content>
<mat-card-actions>
<mat-checkbox
[checked]="isSelected(imageData!.imageView)"
(click)="onImageClick(imageData!.imageView)"
>Main image</mat-checkbox>
</mat-card-actions>
</mat-card>
} }
} }
</div> </div>
<mat-paginator [length]="imageCount()" [pageSize]="pageSize()" [pageSizeOptions]="[3, 6, 9, 12]" (page)="onPage($event)"></mat-paginator> <mat-paginator
[length]="imageCount()"
[pageSize]="pageSize()"
[pageSizeOptions]="[3, 6, 9, 12]"
(page)="onPage($event)"
></mat-paginator>
} }

View File

@ -8,9 +8,8 @@ describe('ImageSelect', () => {
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [ImageSelect] imports: [ImageSelect],
}) }).compileComponents();
.compileComponents();
fixture = TestBed.createComponent(ImageSelect); fixture = TestBed.createComponent(ImageSelect);
component = fixture.componentInstance; component = fixture.componentInstance;

View File

@ -4,12 +4,13 @@ import { ImageService } from '../../../../../shared/services/ImageService';
import { injectQuery, keepPreviousData } from '@tanstack/angular-query-experimental'; import { injectQuery, keepPreviousData } from '@tanstack/angular-query-experimental';
import { Spinner } from '../../../../../shared/components/spinner/spinner'; import { Spinner } from '../../../../../shared/components/spinner/spinner';
import { ImageView } from '../../../../../shared/models/ImageView.model'; import { ImageView } from '../../../../../shared/models/ImageView.model';
import { injectQueries } from '@tanstack/angular-query-experimental/inject-queries-experimental' import { injectQueries } from '@tanstack/angular-query-experimental/inject-queries-experimental';
import { NgClass } from '@angular/common'; import { MatCard, MatCardActions, MatCardContent, MatCardImage } from '@angular/material/card';
import { MatCheckbox } from '@angular/material/checkbox';
@Component({ @Component({
selector: 'app-image-select', selector: 'app-image-select',
imports: [MatPaginator, Spinner, NgClass], imports: [MatPaginator, Spinner, MatCard, MatCardImage, MatCardContent, MatCardActions, MatCheckbox],
templateUrl: './image-select.html', templateUrl: './image-select.html',
styleUrl: './image-select.css', styleUrl: './image-select.css',
}) })

View File

@ -0,0 +1,4 @@
export interface SetImageBody {
username: string;
userFilename: string;
}

View File

@ -7,7 +7,11 @@ import { environment } from '../../../environments/environment';
providedIn: 'root', providedIn: 'root',
}) })
export class EndpointService { export class EndpointService {
public getUrl<P extends readonly string[] = []>(endpoint: keyof typeof Endpoints, pathParts?: string[], queryParams?: QueryParams<P>): string { public getUrl<P extends readonly string[] = []>(
endpoint: keyof typeof Endpoints,
pathParts?: string[],
queryParams?: QueryParams<P>,
): string {
const urlSearchParams = new URLSearchParams(); const urlSearchParams = new URLSearchParams();
if (queryParams?.page !== undefined) { if (queryParams?.page !== undefined) {
urlSearchParams.set('page', queryParams.page.toString()); urlSearchParams.set('page', queryParams.page.toString());

View File

@ -4,7 +4,7 @@ import { firstValueFrom, map } from 'rxjs';
import { EndpointService } from './EndpointService'; import { EndpointService } from './EndpointService';
import { ImageView } from '../models/ImageView.model'; import { ImageView } from '../models/ImageView.model';
import { SliceView } from '../models/SliceView.model'; import { SliceView } from '../models/SliceView.model';
import { QueryParams } from '../models/Query.model';; import { QueryParams } from '../models/Query.model';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
@ -22,7 +22,7 @@ export class ImageService {
'height', 'height',
'width', 'width',
'owner', 'owner',
'viewers' 'viewers',
] as const; ] as const;
private readonly httpClient = inject(HttpClient); private readonly httpClient = inject(HttpClient);
@ -30,7 +30,7 @@ export class ImageService {
public getOwnedImages(queryParams?: QueryParams<typeof ImageService.ImageProps>): Promise<SliceView<ImageView>> { public getOwnedImages(queryParams?: QueryParams<typeof ImageService.ImageProps>): Promise<SliceView<ImageView>> {
return firstValueFrom( return firstValueFrom(
this.httpClient.get<SliceView<ImageView>>(this.endpointService.getUrl('images', [], queryParams)) this.httpClient.get<SliceView<ImageView>>(this.endpointService.getUrl('images', [], queryParams)),
); );
} }

View File

@ -7,6 +7,8 @@ import { RecipeDraftViewModel } from '../models/RecipeDraftView.model';
import { EndpointService } from './EndpointService'; import { EndpointService } from './EndpointService';
import { WithStringDates } from '../util'; import { WithStringDates } from '../util';
import { Recipe } from '../models/Recipe.model'; import { Recipe } from '../models/Recipe.model';
import { ImageView } from '../models/ImageView.model';
import { SetImageBody } from '../models/SetImageBody';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
@ -82,12 +84,21 @@ export class RecipeDraftService {
name: string; name: string;
notes?: string | null; notes?: string | null;
}>; }>;
mainImage?: ImageView | null;
rawText?: string | null; rawText?: string | null;
}, },
): Promise<RecipeUploadClientModel> { ): Promise<RecipeUploadClientModel> {
return firstValueFrom( return firstValueFrom(
this.http this.http
.put<WithStringDates<RecipeDraftViewModel>>(this.endpointService.getUrl('recipeDrafts', [id]), data) .put<WithStringDates<RecipeDraftViewModel>>(this.endpointService.getUrl('recipeDrafts', [id]), {
...data,
mainImage: data.mainImage
? ({
username: data.mainImage.owner.username,
userFilename: data.mainImage.filename,
} satisfies SetImageBody)
: undefined,
})
.pipe( .pipe(
map((rawView) => this.hydrateView(rawView)), map((rawView) => this.hydrateView(rawView)),
map((draft) => ({ map((draft) => ({