Added tanstack query, basic auth logic.
This commit is contained in:
parent
86ef321065
commit
6ae8da85f9
@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"arrowParens": "avoid",
|
||||||
"semi": true,
|
"semi": true,
|
||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
"tabWidth": 4,
|
"tabWidth": 4,
|
||||||
|
|||||||
45
package-lock.json
generated
45
package-lock.json
generated
@ -14,7 +14,7 @@
|
|||||||
"@angular/forms": "^21.0.0",
|
"@angular/forms": "^21.0.0",
|
||||||
"@angular/platform-browser": "^21.0.0",
|
"@angular/platform-browser": "^21.0.0",
|
||||||
"@angular/router": "^21.0.0",
|
"@angular/router": "^21.0.0",
|
||||||
"prettier": "^3.7.4",
|
"@tanstack/angular-query-experimental": "^5.90.16",
|
||||||
"rxjs": "~7.8.0",
|
"rxjs": "~7.8.0",
|
||||||
"tslib": "^2.3.0"
|
"tslib": "^2.3.0"
|
||||||
},
|
},
|
||||||
@ -23,6 +23,7 @@
|
|||||||
"@angular/cli": "^21.0.2",
|
"@angular/cli": "^21.0.2",
|
||||||
"@angular/compiler-cli": "^21.0.0",
|
"@angular/compiler-cli": "^21.0.0",
|
||||||
"jsdom": "^27.1.0",
|
"jsdom": "^27.1.0",
|
||||||
|
"prettier": "^3.7.4",
|
||||||
"typescript": "~5.9.2",
|
"typescript": "~5.9.2",
|
||||||
"vitest": "^4.0.8"
|
"vitest": "^4.0.8"
|
||||||
}
|
}
|
||||||
@ -3891,6 +3892,47 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@tanstack/angular-query-experimental": {
|
||||||
|
"version": "5.90.16",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/angular-query-experimental/-/angular-query-experimental-5.90.16.tgz",
|
||||||
|
"integrity": "sha512-ezXyxuaSA6kpwUxwrxo5Tf3B7KL7mb7cxhLnun/RMlw6h3ZQJzl1cJgrvN97+C0AeHoucmK7RwhEoIY12gY7WA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@tanstack/query-core": "5.90.12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@tanstack/query-devtools": "5.91.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@angular/common": ">=16.0.0",
|
||||||
|
"@angular/core": ">=16.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tanstack/query-core": {
|
||||||
|
"version": "5.90.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.12.tgz",
|
||||||
|
"integrity": "sha512-T1/8t5DhV/SisWjDnaiU2drl6ySvsHj1bHBCWNXd+/T+Hh1cf6JodyEYMd5sgwm+b/mETT4EV3H+zCVczCU5hg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tanstack/query-devtools": {
|
||||||
|
"version": "5.91.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.91.1.tgz",
|
||||||
|
"integrity": "sha512-l8bxjk6BMsCaVQH6NzQEE/bEgFy1hAs5qbgXl0xhzezlaQbPk6Mgz9BqEg2vTLPOHD8N4k+w/gdgCbEzecGyNg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tufjs/canonical-json": {
|
"node_modules/@tufjs/canonical-json": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz",
|
||||||
@ -7272,6 +7314,7 @@
|
|||||||
"version": "3.7.4",
|
"version": "3.7.4",
|
||||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.4.tgz",
|
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.4.tgz",
|
||||||
"integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==",
|
"integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"prettier": "bin/prettier.cjs"
|
"prettier": "bin/prettier.cjs"
|
||||||
|
|||||||
@ -29,6 +29,7 @@
|
|||||||
"@angular/forms": "^21.0.0",
|
"@angular/forms": "^21.0.0",
|
||||||
"@angular/platform-browser": "^21.0.0",
|
"@angular/platform-browser": "^21.0.0",
|
||||||
"@angular/router": "^21.0.0",
|
"@angular/router": "^21.0.0",
|
||||||
|
"@tanstack/angular-query-experimental": "^5.90.16",
|
||||||
"rxjs": "~7.8.0",
|
"rxjs": "~7.8.0",
|
||||||
"tslib": "^2.3.0"
|
"tslib": "^2.3.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -2,7 +2,16 @@ import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/
|
|||||||
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 { authInterceptor } from './interceptor/auth.interceptor';
|
||||||
|
import { provideTanStackQuery, QueryClient } from '@tanstack/angular-query-experimental';
|
||||||
|
import { withDevtools } from '@tanstack/angular-query-experimental/devtools';
|
||||||
|
|
||||||
export const appConfig: ApplicationConfig = {
|
export const appConfig: ApplicationConfig = {
|
||||||
providers: [provideBrowserGlobalErrorListeners(), provideRouter(routes)]
|
providers: [
|
||||||
|
provideBrowserGlobalErrorListeners(),
|
||||||
|
provideRouter(routes),
|
||||||
|
provideHttpClient(withInterceptors([authInterceptor])),
|
||||||
|
provideTanStackQuery(new QueryClient(), withDevtools()),
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
<main>
|
<main>
|
||||||
<h1>Meals Made Easy</h1>
|
<app-header />
|
||||||
<router-outlet />
|
<router-outlet />
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@ -1,9 +1,14 @@
|
|||||||
import { Routes } from '@angular/router';
|
import { Routes } from '@angular/router';
|
||||||
import { RecipeView } from './recipe-view/recipe-view.component';
|
import { RecipeView } from './recipe-view/recipe-view.component';
|
||||||
|
import { RecipesPage } from './recipes-page/recipes-page';
|
||||||
|
|
||||||
export const routes: Routes = [
|
export const routes: Routes = [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: RecipesPage,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'recipes/:username/:slug',
|
path: 'recipes/:username/:slug',
|
||||||
component: RecipeView
|
component: RecipeView,
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
import { Component, signal } from '@angular/core';
|
import { Component, signal } from '@angular/core';
|
||||||
import { RouterOutlet } from '@angular/router';
|
import { RouterOutlet } from '@angular/router';
|
||||||
|
import { Header } from './header/header';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
imports: [RouterOutlet],
|
imports: [RouterOutlet, Header],
|
||||||
templateUrl: './app.html',
|
templateUrl: './app.html',
|
||||||
styleUrl: './app.css',
|
styleUrl: './app.css',
|
||||||
})
|
})
|
||||||
|
|||||||
0
src/app/header/header.css
Normal file
0
src/app/header/header.css
Normal file
7
src/app/header/header.html
Normal file
7
src/app/header/header.html
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<h1>Meals Made Easy</h1>
|
||||||
|
@if (username(); as username) {
|
||||||
|
<h3>Welcome {{ username }}</h3>
|
||||||
|
}
|
||||||
|
<button (click)="loginClick()">Login</button>
|
||||||
|
<button (click)="logoutClick()">Logout</button>
|
||||||
|
<a [routerLink]="'/'">Browse Recipes</a>
|
||||||
22
src/app/header/header.spec.ts
Normal file
22
src/app/header/header.spec.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { Header } from './header';
|
||||||
|
|
||||||
|
describe('Header', () => {
|
||||||
|
let component: Header;
|
||||||
|
let fixture: ComponentFixture<Header>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [Header],
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(Header);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
await fixture.whenStable();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
25
src/app/header/header.ts
Normal file
25
src/app/header/header.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { Component, inject, signal } from '@angular/core';
|
||||||
|
import { AuthService } from '../service/auth.service';
|
||||||
|
import { RouterLink } from '@angular/router';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-header',
|
||||||
|
imports: [RouterLink],
|
||||||
|
templateUrl: './header.html',
|
||||||
|
styleUrl: './header.css',
|
||||||
|
})
|
||||||
|
export class Header {
|
||||||
|
private readonly authService = inject(AuthService);
|
||||||
|
|
||||||
|
protected readonly username = signal<string | null>(null);
|
||||||
|
|
||||||
|
protected async loginClick() {
|
||||||
|
const loginView = await this.authService.login('test-user', 'test');
|
||||||
|
this.username.set(loginView.username);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async logoutClick() {
|
||||||
|
await this.authService.logout();
|
||||||
|
this.username.set(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
17
src/app/interceptor/auth.interceptor.ts
Normal file
17
src/app/interceptor/auth.interceptor.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { HttpInterceptorFn } from '@angular/common/http';
|
||||||
|
import { inject } from '@angular/core';
|
||||||
|
import { AuthStore } from '../service/auth.store';
|
||||||
|
|
||||||
|
export const authInterceptor: HttpInterceptorFn = (req, next) => {
|
||||||
|
const authStore = inject(AuthStore);
|
||||||
|
const token = authStore.getAccessToken();
|
||||||
|
if (token) {
|
||||||
|
return next(
|
||||||
|
req.clone({
|
||||||
|
headers: req.headers.set('Authorization', `Bearer ${token}`),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return next(req);
|
||||||
|
}
|
||||||
|
};
|
||||||
5
src/app/model/LoginView.model.ts
Normal file
5
src/app/model/LoginView.model.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export interface LoginView {
|
||||||
|
username: string;
|
||||||
|
accessToken: string;
|
||||||
|
expires: string;
|
||||||
|
}
|
||||||
@ -14,12 +14,23 @@ export interface RecipeView {
|
|||||||
|
|
||||||
export interface Recipe {
|
export interface Recipe {
|
||||||
id: number;
|
id: number;
|
||||||
title: string;
|
|
||||||
mainImage: ImageView;
|
mainImage: ImageView;
|
||||||
|
owner: ResourceOwner;
|
||||||
|
slug: string;
|
||||||
text: string;
|
text: string;
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResourceOwner {
|
||||||
|
id: number;
|
||||||
|
username: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ImageView {
|
export interface ImageView {
|
||||||
alt: string;
|
alt: string;
|
||||||
|
filename: string;
|
||||||
|
height: number | null;
|
||||||
|
owner: ResourceOwner;
|
||||||
url: string;
|
url: string;
|
||||||
|
width: number | null;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,10 @@
|
|||||||
<h1>{{ recipe.title }}</h1>
|
<h1>{{ recipe.title }}</h1>
|
||||||
<img [ngSrc]="recipe.mainImage.url" [alt]="recipe.mainImage.alt" [width]="600" [height]="400">
|
@if (mainImageUrl.isSuccess()) {
|
||||||
|
<img
|
||||||
|
[src]="mainImageUrl.data()"
|
||||||
|
[alt]="recipe.mainImage.alt"
|
||||||
|
[height]="recipe.mainImage.height"
|
||||||
|
[width]="recipe.mainImage.width"
|
||||||
|
/>
|
||||||
|
}
|
||||||
<div [innerHTML]="recipe.text"></div>
|
<div [innerHTML]="recipe.text"></div>
|
||||||
|
|||||||
@ -8,9 +8,8 @@ describe('Card', () => {
|
|||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [RecipeViewCard]
|
imports: [RecipeViewCard],
|
||||||
})
|
}).compileComponents();
|
||||||
.compileComponents();
|
|
||||||
|
|
||||||
fixture = TestBed.createComponent(RecipeViewCard);
|
fixture = TestBed.createComponent(RecipeViewCard);
|
||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
|
|||||||
@ -1,18 +1,22 @@
|
|||||||
import { Component, Input } from '@angular/core';
|
import { Component, inject, Input } from '@angular/core';
|
||||||
import { Recipe } from '../../model/Recipe.model';
|
import { Recipe } from '../../model/Recipe.model';
|
||||||
import { NgOptimizedImage } from '@angular/common';
|
import { injectQuery } from '@tanstack/angular-query-experimental';
|
||||||
|
import { ImageService } from '../../service/image.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-recipe-view-card',
|
selector: 'app-recipe-view-card',
|
||||||
imports: [
|
imports: [],
|
||||||
NgOptimizedImage
|
|
||||||
],
|
|
||||||
templateUrl: './recipe-view-card.component.html',
|
templateUrl: './recipe-view-card.component.html',
|
||||||
styleUrl: './recipe-view-card.component.css',
|
styleUrl: './recipe-view-card.component.css',
|
||||||
})
|
})
|
||||||
export class RecipeViewCard {
|
export class RecipeViewCard {
|
||||||
|
|
||||||
@Input({ required: true })
|
@Input({ required: true })
|
||||||
public recipe!: Recipe;
|
public recipe!: Recipe;
|
||||||
|
|
||||||
|
private readonly imageService = inject(ImageService);
|
||||||
|
|
||||||
|
protected mainImageUrl = injectQuery(() => ({
|
||||||
|
queryKey: ['images', this.recipe.mainImage.owner.username, this.recipe.mainImage.filename],
|
||||||
|
queryFn: () => this.imageService.getImage(this.recipe.mainImage.url),
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,9 @@
|
|||||||
@if (recipe.isLoading()) {
|
@if (recipe.isLoading()) {
|
||||||
<p>Loading...</p>
|
<p>Loading...</p>
|
||||||
} @else if (recipe.hasValue()) {
|
} @else if (recipe.isSuccess()) {
|
||||||
<app-recipe-view-card [recipe]="recipe.value()"></app-recipe-view-card>
|
<app-recipe-view-card [recipe]="recipe.data()"></app-recipe-view-card>
|
||||||
|
} @else if (recipe.error(); as error) {
|
||||||
|
<p>{{ error }}</p>
|
||||||
|
} @else {
|
||||||
|
<p>There was an error loading the recipe.</p>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,12 +2,11 @@ import { Component, inject, resource } from '@angular/core';
|
|||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { RecipeService } from '../recipe.service';
|
import { RecipeService } from '../recipe.service';
|
||||||
import { RecipeViewCard } from './recipe-view-card/recipe-view-card.component';
|
import { RecipeViewCard } from './recipe-view-card/recipe-view-card.component';
|
||||||
|
import { injectQuery } from '@tanstack/angular-query-experimental';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-recipe-view',
|
selector: 'app-recipe-view',
|
||||||
imports: [
|
imports: [RecipeViewCard],
|
||||||
RecipeViewCard
|
|
||||||
],
|
|
||||||
templateUrl: './recipe-view.component.html',
|
templateUrl: './recipe-view.component.html',
|
||||||
styleUrl: './recipe-view.component.css',
|
styleUrl: './recipe-view.component.css',
|
||||||
})
|
})
|
||||||
@ -17,9 +16,8 @@ export class RecipeView {
|
|||||||
private username = this.route.snapshot.paramMap.get('username') as string;
|
private username = this.route.snapshot.paramMap.get('username') as string;
|
||||||
private slug = this.route.snapshot.paramMap.get('slug') as string;
|
private slug = this.route.snapshot.paramMap.get('slug') as string;
|
||||||
|
|
||||||
protected recipe = resource({
|
protected recipe = injectQuery(() => ({
|
||||||
loader: () => {
|
queryKey: ['recipe', this.username, this.slug],
|
||||||
return this.recipeService.getRecipe(this.username, this.slug);
|
queryFn: () => this.recipeService.getRecipe(this.username, this.slug),
|
||||||
},
|
}));
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 { map, Observable } from 'rxjs';
|
import { firstValueFrom, map } from 'rxjs';
|
||||||
import { Recipe, RecipeInfoViews, RecipeView } from './model/Recipe.model';
|
import { Recipe, RecipeInfoViews, RecipeView } from './model/Recipe.model';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
@ -9,15 +9,19 @@ import { Recipe, RecipeInfoViews, RecipeView } from './model/Recipe.model';
|
|||||||
export class RecipeService {
|
export class RecipeService {
|
||||||
private readonly http = inject(HttpClient);
|
private readonly http = inject(HttpClient);
|
||||||
|
|
||||||
public getRecipes(): Observable<Recipe[]> {
|
public getRecipes(): Promise<Recipe[]> {
|
||||||
return this.http
|
return firstValueFrom(
|
||||||
|
this.http
|
||||||
.get<RecipeInfoViews>('http://localhost:8080/recipes')
|
.get<RecipeInfoViews>('http://localhost:8080/recipes')
|
||||||
.pipe(map((res) => res.content));
|
.pipe(map((res) => res.content)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getRecipe(username: string, slug: string): Promise<Recipe> {
|
public async getRecipe(username: string, slug: string): Promise<Recipe> {
|
||||||
const res = await fetch(`http://localhost:8080/recipes/${username}/${slug}`)
|
return firstValueFrom(
|
||||||
const recipeView = await res.json() as RecipeView;
|
this.http
|
||||||
return recipeView.recipe;
|
.get<RecipeView>(`http://localhost:8080/recipes/${username}/${slug}`)
|
||||||
|
.pipe(map((recipeView) => recipeView.recipe)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
0
src/app/recipes-page/recipes-page.css
Normal file
0
src/app/recipes-page/recipes-page.css
Normal file
11
src/app/recipes-page/recipes-page.html
Normal file
11
src/app/recipes-page/recipes-page.html
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
@if (recipes.isSuccess()) {
|
||||||
|
<ul>
|
||||||
|
@for (recipe of recipes.data(); track recipe.id) {
|
||||||
|
<li>
|
||||||
|
<a [routerLink]="['recipes', recipe.owner.username, recipe.slug]">{{
|
||||||
|
recipe.title
|
||||||
|
}}</a>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
}
|
||||||
22
src/app/recipes-page/recipes-page.spec.ts
Normal file
22
src/app/recipes-page/recipes-page.spec.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { RecipesPage } from './recipes-page';
|
||||||
|
|
||||||
|
describe('RecipesPage', () => {
|
||||||
|
let component: RecipesPage;
|
||||||
|
let fixture: ComponentFixture<RecipesPage>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [RecipesPage],
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(RecipesPage);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
await fixture.whenStable();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
19
src/app/recipes-page/recipes-page.ts
Normal file
19
src/app/recipes-page/recipes-page.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { Component, inject } from '@angular/core';
|
||||||
|
import { RecipeService } from '../recipe.service';
|
||||||
|
import { injectQuery } from '@tanstack/angular-query-experimental';
|
||||||
|
import { RouterLink } from '@angular/router';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-recipes-page',
|
||||||
|
imports: [RouterLink],
|
||||||
|
templateUrl: './recipes-page.html',
|
||||||
|
styleUrl: './recipes-page.css',
|
||||||
|
})
|
||||||
|
export class RecipesPage {
|
||||||
|
private readonly recipeService = inject(RecipeService);
|
||||||
|
|
||||||
|
protected readonly recipes = injectQuery(() => ({
|
||||||
|
queryKey: ['recipes'],
|
||||||
|
queryFn: () => this.recipeService.getRecipes(),
|
||||||
|
}));
|
||||||
|
}
|
||||||
16
src/app/service/auth.service.spec.ts
Normal file
16
src/app/service/auth.service.spec.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { AuthService } from './auth.service';
|
||||||
|
|
||||||
|
describe('LoginService', () => {
|
||||||
|
let service: AuthService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
service = TestBed.inject(AuthService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
38
src/app/service/auth.service.ts
Normal file
38
src/app/service/auth.service.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { inject, Injectable } from '@angular/core';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { LoginView } from '../model/LoginView.model';
|
||||||
|
import { firstValueFrom, tap } from 'rxjs';
|
||||||
|
import { AuthStore } from './auth.store';
|
||||||
|
import { QueryClient } from '@tanstack/angular-query-experimental';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class AuthService {
|
||||||
|
private readonly http = inject(HttpClient);
|
||||||
|
private readonly authStore = inject(AuthStore);
|
||||||
|
private readonly queryClient = inject(QueryClient);
|
||||||
|
private readonly router = inject(Router);
|
||||||
|
|
||||||
|
public async login(username: string, password: string): Promise<LoginView> {
|
||||||
|
const loginView = await firstValueFrom(
|
||||||
|
this.http
|
||||||
|
.post<LoginView>('http://localhost:8080/auth/login', { username, password })
|
||||||
|
.pipe(
|
||||||
|
tap((loginView) => {
|
||||||
|
this.authStore.setAccessToken(loginView.accessToken);
|
||||||
|
this.authStore.setUsername(loginView.username);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await this.queryClient.invalidateQueries();
|
||||||
|
return loginView;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async logout(): Promise<void> {
|
||||||
|
await firstValueFrom(this.http.post('http://localhost:8080/auth/logout', null));
|
||||||
|
await this.queryClient.invalidateQueries();
|
||||||
|
await this.router.navigate(['/']);
|
||||||
|
}
|
||||||
|
}
|
||||||
25
src/app/service/auth.store.ts
Normal file
25
src/app/service/auth.store.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { Injectable, signal } from '@angular/core';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class AuthStore {
|
||||||
|
private readonly accessToken = signal<string | null>(null);
|
||||||
|
private readonly username = signal<string | null>(null);
|
||||||
|
|
||||||
|
public setAccessToken(accessToken: string | null): void {
|
||||||
|
this.accessToken.set(accessToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getAccessToken(): string | null {
|
||||||
|
return this.accessToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
public setUsername(username: string | null): void {
|
||||||
|
this.username.set(username);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getUsername(): string | null {
|
||||||
|
return this.username();
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/app/service/image.service.ts
Normal file
20
src/app/service/image.service.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { inject, Injectable } from '@angular/core';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { firstValueFrom, map } from 'rxjs';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class ImageService {
|
||||||
|
private readonly httpClient = inject(HttpClient);
|
||||||
|
|
||||||
|
public getImage(backendUrl: string): Promise<string> {
|
||||||
|
return firstValueFrom(
|
||||||
|
this.httpClient
|
||||||
|
.get(backendUrl, {
|
||||||
|
responseType: 'blob',
|
||||||
|
})
|
||||||
|
.pipe(map((blob) => URL.createObjectURL(blob))),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user