From 625b38701f53ca08d28f1f92fc1b07611766f5fc Mon Sep 17 00:00:00 2001 From: Jesse Brault Date: Mon, 29 Dec 2025 12:43:26 -0600 Subject: [PATCH] Add ai-search page. --- src/app/app.routes.ts | 5 +++ src/app/nav/nav.html | 1 + src/app/pipe/dateTimeFormat.pipe.ts | 4 +-- src/app/recipe-card-grid/recipe-card-grid.css | 17 ++++++++++ .../recipe-card-grid/recipe-card-grid.html | 5 +++ .../recipe-card-grid/recipe-card-grid.spec.ts | 22 +++++++++++++ src/app/recipe-card-grid/recipe-card-grid.ts | 13 ++++++++ .../recipe-card/recipe-card.css | 0 .../recipe-card/recipe-card.html | 0 .../recipe-card/recipe-card.spec.ts | 0 .../recipe-card/recipe-card.ts | 0 .../recipe-comments-list.component.html | 4 ++- .../recipe-comments-list.component.ts | 2 +- src/app/recipes-page/recipes-page.css | 17 ---------- src/app/recipes-page/recipes-page.html | 6 +--- src/app/recipes-page/recipes-page.ts | 4 +-- .../recipes-search-page.css | 0 .../recipes-search-page.html | 16 +++++++++ .../recipes-search-page.spec.ts | 22 +++++++++++++ .../recipes-search-page.ts | 33 +++++++++++++++++++ src/app/service/date.service.ts | 6 ++-- src/app/service/endpoint.service.ts | 2 +- src/app/service/recipe.service.ts | 12 +++++++ 23 files changed, 157 insertions(+), 34 deletions(-) create mode 100644 src/app/recipe-card-grid/recipe-card-grid.css create mode 100644 src/app/recipe-card-grid/recipe-card-grid.html create mode 100644 src/app/recipe-card-grid/recipe-card-grid.spec.ts create mode 100644 src/app/recipe-card-grid/recipe-card-grid.ts rename src/app/{recipes-page => recipe-card-grid}/recipe-card/recipe-card.css (100%) rename src/app/{recipes-page => recipe-card-grid}/recipe-card/recipe-card.html (100%) rename src/app/{recipes-page => recipe-card-grid}/recipe-card/recipe-card.spec.ts (100%) rename src/app/{recipes-page => recipe-card-grid}/recipe-card/recipe-card.ts (100%) create mode 100644 src/app/recipes-search-page/recipes-search-page.css create mode 100644 src/app/recipes-search-page/recipes-search-page.html create mode 100644 src/app/recipes-search-page/recipes-search-page.spec.ts create mode 100644 src/app/recipes-search-page/recipes-search-page.ts diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 08ec720..324612b 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -1,12 +1,17 @@ import { Routes } from '@angular/router'; import { RecipePage } from './recipe-page/recipe-page'; import { RecipesPage } from './recipes-page/recipes-page'; +import { RecipesSearchPage } from './recipes-search-page/recipes-search-page'; export const routes: Routes = [ { path: '', component: RecipesPage, }, + { + path: 'recipes-search', + component: RecipesSearchPage, + }, { path: 'recipes/:username/:slug', component: RecipePage, diff --git a/src/app/nav/nav.html b/src/app/nav/nav.html index b2f5a6f..11bc2f4 100644 --- a/src/app/nav/nav.html +++ b/src/app/nav/nav.html @@ -2,5 +2,6 @@

Nav

diff --git a/src/app/pipe/dateTimeFormat.pipe.ts b/src/app/pipe/dateTimeFormat.pipe.ts index aae77b2..c05f698 100644 --- a/src/app/pipe/dateTimeFormat.pipe.ts +++ b/src/app/pipe/dateTimeFormat.pipe.ts @@ -2,14 +2,12 @@ import { inject, Pipe, PipeTransform } from '@angular/core'; import { DateService } from '../service/date.service'; @Pipe({ - name: 'dateTimeFormat' + name: 'dateTimeFormat', }) export class DateTimeFormatPipe implements PipeTransform { - private readonly dateService = inject(DateService); public transform(raw: string): string { return this.dateService.format(raw); } - } diff --git a/src/app/recipe-card-grid/recipe-card-grid.css b/src/app/recipe-card-grid/recipe-card-grid.css new file mode 100644 index 0000000..42e694b --- /dev/null +++ b/src/app/recipe-card-grid/recipe-card-grid.css @@ -0,0 +1,17 @@ +.recipe-card-grid { + display: grid; + gap: 20px; + grid-template-columns: repeat(2, 1fr); +} + +@media screen and (min-width: 1300px) { + .recipe-card-grid { + grid-template-columns: repeat(3, 1fr); + } +} + +@media screen and (min-width: 1700px) { + .recipe-card-grid { + grid-template-columns: repeat(4, 1fr); + } +} diff --git a/src/app/recipe-card-grid/recipe-card-grid.html b/src/app/recipe-card-grid/recipe-card-grid.html new file mode 100644 index 0000000..e9a31ca --- /dev/null +++ b/src/app/recipe-card-grid/recipe-card-grid.html @@ -0,0 +1,5 @@ +
+ @for (recipe of recipes(); track recipe.id) { + + } +
diff --git a/src/app/recipe-card-grid/recipe-card-grid.spec.ts b/src/app/recipe-card-grid/recipe-card-grid.spec.ts new file mode 100644 index 0000000..e9769d9 --- /dev/null +++ b/src/app/recipe-card-grid/recipe-card-grid.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { RecipeCardGrid } from './recipe-card-grid'; + +describe('RecipeCardGrid', () => { + let component: RecipeCardGrid; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [RecipeCardGrid], + }).compileComponents(); + + fixture = TestBed.createComponent(RecipeCardGrid); + component = fixture.componentInstance; + await fixture.whenStable(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/recipe-card-grid/recipe-card-grid.ts b/src/app/recipe-card-grid/recipe-card-grid.ts new file mode 100644 index 0000000..046baf9 --- /dev/null +++ b/src/app/recipe-card-grid/recipe-card-grid.ts @@ -0,0 +1,13 @@ +import { Component, input } from '@angular/core'; +import { RecipeCard } from './recipe-card/recipe-card'; +import { Recipe } from '../model/Recipe.model'; + +@Component({ + selector: 'app-recipe-card-grid', + imports: [RecipeCard], + templateUrl: './recipe-card-grid.html', + styleUrl: './recipe-card-grid.css', +}) +export class RecipeCardGrid { + public readonly recipes = input.required(); +} diff --git a/src/app/recipes-page/recipe-card/recipe-card.css b/src/app/recipe-card-grid/recipe-card/recipe-card.css similarity index 100% rename from src/app/recipes-page/recipe-card/recipe-card.css rename to src/app/recipe-card-grid/recipe-card/recipe-card.css diff --git a/src/app/recipes-page/recipe-card/recipe-card.html b/src/app/recipe-card-grid/recipe-card/recipe-card.html similarity index 100% rename from src/app/recipes-page/recipe-card/recipe-card.html rename to src/app/recipe-card-grid/recipe-card/recipe-card.html diff --git a/src/app/recipes-page/recipe-card/recipe-card.spec.ts b/src/app/recipe-card-grid/recipe-card/recipe-card.spec.ts similarity index 100% rename from src/app/recipes-page/recipe-card/recipe-card.spec.ts rename to src/app/recipe-card-grid/recipe-card/recipe-card.spec.ts diff --git a/src/app/recipes-page/recipe-card/recipe-card.ts b/src/app/recipe-card-grid/recipe-card/recipe-card.ts similarity index 100% rename from src/app/recipes-page/recipe-card/recipe-card.ts rename to src/app/recipe-card-grid/recipe-card/recipe-card.ts diff --git a/src/app/recipe-comments-list/recipe-comments-list.component.html b/src/app/recipe-comments-list/recipe-comments-list.component.html index 7b79ca5..7d59a85 100644 --- a/src/app/recipe-comments-list/recipe-comments-list.component.html +++ b/src/app/recipe-comments-list/recipe-comments-list.component.html @@ -31,7 +31,9 @@
  • {{ recipeComment.owner.username }} - {{ recipeComment.created | dateTimeFormat }} + {{ + recipeComment.created | dateTimeFormat + }}
  • diff --git a/src/app/recipe-comments-list/recipe-comments-list.component.ts b/src/app/recipe-comments-list/recipe-comments-list.component.ts index 6713366..fc160a8 100644 --- a/src/app/recipe-comments-list/recipe-comments-list.component.ts +++ b/src/app/recipe-comments-list/recipe-comments-list.component.ts @@ -47,7 +47,7 @@ export class RecipeCommentsList { protected readonly addCommentMutation = injectMutation(() => ({ mutationFn: (commentText: string) => - this.recipeService.addComment(this.recipeUsername(), this.recipeSlug(), commentText) + this.recipeService.addComment(this.recipeUsername(), this.recipeSlug(), commentText), })); protected onCommentSubmit() { diff --git a/src/app/recipes-page/recipes-page.css b/src/app/recipes-page/recipes-page.css index 42e694b..e69de29 100644 --- a/src/app/recipes-page/recipes-page.css +++ b/src/app/recipes-page/recipes-page.css @@ -1,17 +0,0 @@ -.recipe-card-grid { - display: grid; - gap: 20px; - grid-template-columns: repeat(2, 1fr); -} - -@media screen and (min-width: 1300px) { - .recipe-card-grid { - grid-template-columns: repeat(3, 1fr); - } -} - -@media screen and (min-width: 1700px) { - .recipe-card-grid { - grid-template-columns: repeat(4, 1fr); - } -} diff --git a/src/app/recipes-page/recipes-page.html b/src/app/recipes-page/recipes-page.html index 79cd5db..935f60d 100644 --- a/src/app/recipes-page/recipes-page.html +++ b/src/app/recipes-page/recipes-page.html @@ -1,10 +1,6 @@

    Recipes

    @if (recipes.isSuccess()) { -
    - @for (recipe of recipes.data(); track recipe.id) { - - } -
    + } @else if (recipes.isLoading()) {

    Loading...

    } @else if (recipes.isError()) { diff --git a/src/app/recipes-page/recipes-page.ts b/src/app/recipes-page/recipes-page.ts index e6cc3d5..f48faf8 100644 --- a/src/app/recipes-page/recipes-page.ts +++ b/src/app/recipes-page/recipes-page.ts @@ -1,11 +1,11 @@ import { Component, inject } from '@angular/core'; import { RecipeService } from '../service/recipe.service'; import { injectQuery } from '@tanstack/angular-query-experimental'; -import { RecipeCard } from './recipe-card/recipe-card'; +import { RecipeCardGrid } from '../recipe-card-grid/recipe-card-grid'; @Component({ selector: 'app-recipes-page', - imports: [RecipeCard], + imports: [RecipeCardGrid], templateUrl: './recipes-page.html', styleUrl: './recipes-page.css', }) diff --git a/src/app/recipes-search-page/recipes-search-page.css b/src/app/recipes-search-page/recipes-search-page.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/recipes-search-page/recipes-search-page.html b/src/app/recipes-search-page/recipes-search-page.html new file mode 100644 index 0000000..9ecb80a --- /dev/null +++ b/src/app/recipes-search-page/recipes-search-page.html @@ -0,0 +1,16 @@ +

    Search Recipes

    +
    + + + +
    +@if (givenPrompt() !== null) { + @if (resultsQuery.isLoading()) { +

    Loading search results...

    + } @else if (resultsQuery.isSuccess()) { +

    Showing results for {{ givenPrompt() }}

    + + } @else if (resultsQuery.isError()) { +

    There was an error during search.

    + } +} diff --git a/src/app/recipes-search-page/recipes-search-page.spec.ts b/src/app/recipes-search-page/recipes-search-page.spec.ts new file mode 100644 index 0000000..5e3539f --- /dev/null +++ b/src/app/recipes-search-page/recipes-search-page.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { RecipesSearchPage } from './recipes-search-page'; + +describe('RecipesSearchPage', () => { + let component: RecipesSearchPage; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [RecipesSearchPage], + }).compileComponents(); + + fixture = TestBed.createComponent(RecipesSearchPage); + component = fixture.componentInstance; + await fixture.whenStable(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/recipes-search-page/recipes-search-page.ts b/src/app/recipes-search-page/recipes-search-page.ts new file mode 100644 index 0000000..7715ffa --- /dev/null +++ b/src/app/recipes-search-page/recipes-search-page.ts @@ -0,0 +1,33 @@ +import { Component, inject, signal } from '@angular/core'; +import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; +import { injectQuery } from '@tanstack/angular-query-experimental'; +import { RecipeService } from '../service/recipe.service'; +import { RecipeCardGrid } from '../recipe-card-grid/recipe-card-grid'; + +@Component({ + selector: 'app-recipes-search-page', + imports: [ReactiveFormsModule, RecipeCardGrid], + templateUrl: './recipes-search-page.html', + styleUrl: './recipes-search-page.css', +}) +export class RecipesSearchPage { + private readonly recipeService = inject(RecipeService); + + protected readonly searchRecipesForm = new FormGroup({ + prompt: new FormControl('', [Validators.required]), + }); + + protected readonly givenPrompt = signal(null); + + protected readonly resultsQuery = injectQuery(() => ({ + queryFn: () => this.recipeService.aiSearch(this.givenPrompt()!), + queryKey: ['recipes-search', this.givenPrompt()], + enabled: () => !!this.givenPrompt(), + })); + + protected onPromptSubmit() { + if (this.searchRecipesForm.value.prompt) { + this.givenPrompt.set(this.searchRecipesForm.value.prompt); + } + } +} diff --git a/src/app/service/date.service.ts b/src/app/service/date.service.ts index c6e29d1..45c6765 100644 --- a/src/app/service/date.service.ts +++ b/src/app/service/date.service.ts @@ -1,17 +1,15 @@ import { Injectable } from '@angular/core'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class DateService { - private readonly dateTimeFormat = Intl.DateTimeFormat('en-US', { dateStyle: 'long', - timeStyle: 'short' + timeStyle: 'short', }); public format(raw: string): string { return this.dateTimeFormat.format(new Date(raw)); } - } diff --git a/src/app/service/endpoint.service.ts b/src/app/service/endpoint.service.ts index a8be3d5..25327ff 100644 --- a/src/app/service/endpoint.service.ts +++ b/src/app/service/endpoint.service.ts @@ -33,7 +33,7 @@ export class EndpointService { } }); - let pathString = pathParams?.join('/'); + let pathString = pathParams?.join('/') || ''; if (pathString?.length) { pathString = '/' + pathString; } diff --git a/src/app/service/recipe.service.ts b/src/app/service/recipe.service.ts index 0e1a1e1..2aa285d 100644 --- a/src/app/service/recipe.service.ts +++ b/src/app/service/recipe.service.ts @@ -80,4 +80,16 @@ export class RecipeService { }); return comment; } + + public async aiSearch(prompt: string): Promise { + const recipeInfoViews = await firstValueFrom( + this.http.post<{ results: Recipe[] }>(this.endpointService.getUrl('recipes'), { + type: 'AI_PROMPT', + data: { + prompt, + }, + }), + ); + return recipeInfoViews.results; + } }