Added tanstack query, basic auth logic.
This commit is contained in:
parent
86ef321065
commit
6ae8da85f9
@ -1,4 +1,5 @@
|
||||
{
|
||||
"arrowParens": "avoid",
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 4,
|
||||
|
||||
45
package-lock.json
generated
45
package-lock.json
generated
@ -14,7 +14,7 @@
|
||||
"@angular/forms": "^21.0.0",
|
||||
"@angular/platform-browser": "^21.0.0",
|
||||
"@angular/router": "^21.0.0",
|
||||
"prettier": "^3.7.4",
|
||||
"@tanstack/angular-query-experimental": "^5.90.16",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
@ -23,6 +23,7 @@
|
||||
"@angular/cli": "^21.0.2",
|
||||
"@angular/compiler-cli": "^21.0.0",
|
||||
"jsdom": "^27.1.0",
|
||||
"prettier": "^3.7.4",
|
||||
"typescript": "~5.9.2",
|
||||
"vitest": "^4.0.8"
|
||||
}
|
||||
@ -3891,6 +3892,47 @@
|
||||
"license": "MIT",
|
||||
"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": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz",
|
||||
@ -7272,6 +7314,7 @@
|
||||
"version": "3.7.4",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.4.tgz",
|
||||
"integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
|
||||
@ -29,6 +29,7 @@
|
||||
"@angular/forms": "^21.0.0",
|
||||
"@angular/platform-browser": "^21.0.0",
|
||||
"@angular/router": "^21.0.0",
|
||||
"@tanstack/angular-query-experimental": "^5.90.16",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
|
||||
@ -2,7 +2,16 @@ import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/
|
||||
import { provideRouter } from '@angular/router';
|
||||
|
||||
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 = {
|
||||
providers: [provideBrowserGlobalErrorListeners(), provideRouter(routes)]
|
||||
providers: [
|
||||
provideBrowserGlobalErrorListeners(),
|
||||
provideRouter(routes),
|
||||
provideHttpClient(withInterceptors([authInterceptor])),
|
||||
provideTanStackQuery(new QueryClient(), withDevtools()),
|
||||
],
|
||||
};
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
<main>
|
||||
<h1>Meals Made Easy</h1>
|
||||
<app-header />
|
||||
<router-outlet />
|
||||
</main>
|
||||
|
||||
@ -1,9 +1,14 @@
|
||||
import { Routes } from '@angular/router';
|
||||
import { RecipeView } from './recipe-view/recipe-view.component';
|
||||
import { RecipesPage } from './recipes-page/recipes-page';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: RecipesPage,
|
||||
},
|
||||
{
|
||||
path: 'recipes/:username/:slug',
|
||||
component: RecipeView
|
||||
}
|
||||
component: RecipeView,
|
||||
},
|
||||
];
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
import { Component, signal } from '@angular/core';
|
||||
import { RouterOutlet } from '@angular/router';
|
||||
import { Header } from './header/header';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
imports: [RouterOutlet],
|
||||
imports: [RouterOutlet, Header],
|
||||
templateUrl: './app.html',
|
||||
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 {
|
||||
id: number;
|
||||
title: string;
|
||||
mainImage: ImageView;
|
||||
owner: ResourceOwner;
|
||||
slug: string;
|
||||
text: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export interface ResourceOwner {
|
||||
id: number;
|
||||
username: string;
|
||||
}
|
||||
|
||||
export interface ImageView {
|
||||
alt: string;
|
||||
filename: string;
|
||||
height: number | null;
|
||||
owner: ResourceOwner;
|
||||
url: string;
|
||||
width: number | null;
|
||||
}
|
||||
|
||||
@ -1,3 +1,10 @@
|
||||
<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>
|
||||
|
||||
@ -3,21 +3,20 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { RecipeViewCard } from './recipe-view-card.component';
|
||||
|
||||
describe('Card', () => {
|
||||
let component: RecipeViewCard;
|
||||
let fixture: ComponentFixture<RecipeViewCard>;
|
||||
let component: RecipeViewCard;
|
||||
let fixture: ComponentFixture<RecipeViewCard>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [RecipeViewCard]
|
||||
})
|
||||
.compileComponents();
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [RecipeViewCard],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(RecipeViewCard);
|
||||
component = fixture.componentInstance;
|
||||
await fixture.whenStable();
|
||||
});
|
||||
fixture = TestBed.createComponent(RecipeViewCard);
|
||||
component = fixture.componentInstance;
|
||||
await fixture.whenStable();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,18 +1,22 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { Component, inject, Input } from '@angular/core';
|
||||
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({
|
||||
selector: 'app-recipe-view-card',
|
||||
imports: [
|
||||
NgOptimizedImage
|
||||
],
|
||||
templateUrl: './recipe-view-card.component.html',
|
||||
styleUrl: './recipe-view-card.component.css',
|
||||
selector: 'app-recipe-view-card',
|
||||
imports: [],
|
||||
templateUrl: './recipe-view-card.component.html',
|
||||
styleUrl: './recipe-view-card.component.css',
|
||||
})
|
||||
export class RecipeViewCard {
|
||||
|
||||
@Input({ required: true })
|
||||
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()) {
|
||||
<p>Loading...</p>
|
||||
} @else if (recipe.hasValue()) {
|
||||
<app-recipe-view-card [recipe]="recipe.value()"></app-recipe-view-card>
|
||||
} @else if (recipe.isSuccess()) {
|
||||
<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 { RecipeService } from '../recipe.service';
|
||||
import { RecipeViewCard } from './recipe-view-card/recipe-view-card.component';
|
||||
import { injectQuery } from '@tanstack/angular-query-experimental';
|
||||
|
||||
@Component({
|
||||
selector: 'app-recipe-view',
|
||||
imports: [
|
||||
RecipeViewCard
|
||||
],
|
||||
imports: [RecipeViewCard],
|
||||
templateUrl: './recipe-view.component.html',
|
||||
styleUrl: './recipe-view.component.css',
|
||||
})
|
||||
@ -17,9 +16,8 @@ export class RecipeView {
|
||||
private username = this.route.snapshot.paramMap.get('username') as string;
|
||||
private slug = this.route.snapshot.paramMap.get('slug') as string;
|
||||
|
||||
protected recipe = resource({
|
||||
loader: () => {
|
||||
return this.recipeService.getRecipe(this.username, this.slug);
|
||||
},
|
||||
});
|
||||
protected recipe = injectQuery(() => ({
|
||||
queryKey: ['recipe', this.username, this.slug],
|
||||
queryFn: () => this.recipeService.getRecipe(this.username, this.slug),
|
||||
}));
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { map, Observable } from 'rxjs';
|
||||
import { firstValueFrom, map } from 'rxjs';
|
||||
import { Recipe, RecipeInfoViews, RecipeView } from './model/Recipe.model';
|
||||
|
||||
@Injectable({
|
||||
@ -9,15 +9,19 @@ import { Recipe, RecipeInfoViews, RecipeView } from './model/Recipe.model';
|
||||
export class RecipeService {
|
||||
private readonly http = inject(HttpClient);
|
||||
|
||||
public getRecipes(): Observable<Recipe[]> {
|
||||
return this.http
|
||||
.get<RecipeInfoViews>('http://localhost:8080/recipes')
|
||||
.pipe(map((res) => res.content));
|
||||
public getRecipes(): Promise<Recipe[]> {
|
||||
return firstValueFrom(
|
||||
this.http
|
||||
.get<RecipeInfoViews>('http://localhost:8080/recipes')
|
||||
.pipe(map((res) => res.content)),
|
||||
);
|
||||
}
|
||||
|
||||
public async getRecipe(username: string, slug: string): Promise<Recipe> {
|
||||
const res = await fetch(`http://localhost:8080/recipes/${username}/${slug}`)
|
||||
const recipeView = await res.json() as RecipeView;
|
||||
return recipeView.recipe;
|
||||
return firstValueFrom(
|
||||
this.http
|
||||
.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