Add ai-search page.
This commit is contained in:
parent
0699c704de
commit
625b38701f
@ -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,
|
||||
|
||||
@ -2,5 +2,6 @@
|
||||
<h2>Nav</h2>
|
||||
<ul>
|
||||
<li><a [routerLink]="'/'">Browse Recipes</a></li>
|
||||
<li><a [routerLink]="'/recipes-search'">Search Recipes</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
17
src/app/recipe-card-grid/recipe-card-grid.css
Normal file
17
src/app/recipe-card-grid/recipe-card-grid.css
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
5
src/app/recipe-card-grid/recipe-card-grid.html
Normal file
5
src/app/recipe-card-grid/recipe-card-grid.html
Normal file
@ -0,0 +1,5 @@
|
||||
<section class="recipe-card-grid">
|
||||
@for (recipe of recipes(); track recipe.id) {
|
||||
<app-recipe-card [recipe]="recipe" />
|
||||
}
|
||||
</section>
|
||||
22
src/app/recipe-card-grid/recipe-card-grid.spec.ts
Normal file
22
src/app/recipe-card-grid/recipe-card-grid.spec.ts
Normal file
@ -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<RecipeCardGrid>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [RecipeCardGrid],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(RecipeCardGrid);
|
||||
component = fixture.componentInstance;
|
||||
await fixture.whenStable();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
13
src/app/recipe-card-grid/recipe-card-grid.ts
Normal file
13
src/app/recipe-card-grid/recipe-card-grid.ts
Normal file
@ -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<Recipe[]>();
|
||||
}
|
||||
@ -31,7 +31,9 @@
|
||||
<li class="comment">
|
||||
<div class="comment-username-time">
|
||||
<span class="comment-username">{{ recipeComment.owner.username }}</span>
|
||||
<span class="comment-time">{{ recipeComment.created | dateTimeFormat }}</span>
|
||||
<span class="comment-time">{{
|
||||
recipeComment.created | dateTimeFormat
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="comment-text" [innerHTML]="recipeComment.text"></div>
|
||||
</li>
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -1,10 +1,6 @@
|
||||
<h1>Recipes</h1>
|
||||
@if (recipes.isSuccess()) {
|
||||
<section class="recipe-card-grid">
|
||||
@for (recipe of recipes.data(); track recipe.id) {
|
||||
<app-recipe-card [recipe]="recipe" />
|
||||
}
|
||||
</section>
|
||||
<app-recipe-card-grid [recipes]="recipes.data()" />
|
||||
} @else if (recipes.isLoading()) {
|
||||
<p>Loading...</p>
|
||||
} @else if (recipes.isError()) {
|
||||
|
||||
@ -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',
|
||||
})
|
||||
|
||||
0
src/app/recipes-search-page/recipes-search-page.css
Normal file
0
src/app/recipes-search-page/recipes-search-page.css
Normal file
16
src/app/recipes-search-page/recipes-search-page.html
Normal file
16
src/app/recipes-search-page/recipes-search-page.html
Normal file
@ -0,0 +1,16 @@
|
||||
<h1>Search Recipes</h1>
|
||||
<form id="search-recipes-form" [formGroup]="searchRecipesForm" (ngSubmit)="onPromptSubmit()">
|
||||
<label for="prompt">Search Prompt:</label>
|
||||
<input id="prompt" formControlName="prompt" type="text" />
|
||||
<button type="submit" [disabled]="!searchRecipesForm.valid">Search</button>
|
||||
</form>
|
||||
@if (givenPrompt() !== null) {
|
||||
@if (resultsQuery.isLoading()) {
|
||||
<p>Loading search results...</p>
|
||||
} @else if (resultsQuery.isSuccess()) {
|
||||
<p>Showing results for {{ givenPrompt() }}</p>
|
||||
<app-recipe-card-grid [recipes]="resultsQuery.data()" />
|
||||
} @else if (resultsQuery.isError()) {
|
||||
<p>There was an error during search.</p>
|
||||
}
|
||||
}
|
||||
22
src/app/recipes-search-page/recipes-search-page.spec.ts
Normal file
22
src/app/recipes-search-page/recipes-search-page.spec.ts
Normal file
@ -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<RecipesSearchPage>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [RecipesSearchPage],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(RecipesSearchPage);
|
||||
component = fixture.componentInstance;
|
||||
await fixture.whenStable();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
33
src/app/recipes-search-page/recipes-search-page.ts
Normal file
33
src/app/recipes-search-page/recipes-search-page.ts
Normal file
@ -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 | string>(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -33,7 +33,7 @@ export class EndpointService {
|
||||
}
|
||||
});
|
||||
|
||||
let pathString = pathParams?.join('/');
|
||||
let pathString = pathParams?.join('/') || '';
|
||||
if (pathString?.length) {
|
||||
pathString = '/' + pathString;
|
||||
}
|
||||
|
||||
@ -80,4 +80,16 @@ export class RecipeService {
|
||||
});
|
||||
return comment;
|
||||
}
|
||||
|
||||
public async aiSearch(prompt: string): Promise<Recipe[]> {
|
||||
const recipeInfoViews = await firstValueFrom(
|
||||
this.http.post<{ results: Recipe[] }>(this.endpointService.getUrl('recipes'), {
|
||||
type: 'AI_PROMPT',
|
||||
data: {
|
||||
prompt,
|
||||
},
|
||||
}),
|
||||
);
|
||||
return recipeInfoViews.results;
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user