Skip to content

Commit e7d81fd

Browse files
authored
Merge pull request #18 from in2workspace/feature/dashboard
Feature/dashboard
2 parents bbd085a + f393d24 commit e7d81fd

14 files changed

+333
-397
lines changed

docker-compose.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@ name: isbe-catalog-ui
22

33
services:
44
catalog-ui:
5-
image: in2workspace/bae-frontend:v0.0.2
5+
image: in2workspace/bae-frontend:v3
66
container_name: isbe-catalog-ui
77
restart: always

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "bae-frontend",
3-
"version": "0.0.2",
3+
"version": "0.0.3",
44
"scripts": {
55
"ng": "ng",
66
"start": "ng serve",

src/app/app.component.html

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
<bae-header class="fixed w-full z-50 top-0 start-0"></bae-header>
22
<main class="bg-fixed bg-no-repeat bg-right pb-[25px] pt-[75px]">
33
<router-outlet></router-outlet>
4-
</main>
5-
<bae-footer class="hidden md:block"></bae-footer>
4+
</main>

src/app/app.routes.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { NgModule } from '@angular/core';
22
import { RouterModule, Routes } from '@angular/router';
33

44
import { DashboardComponent } from './pages/dashboard/dashboard.component';
5-
import { SearchComponent } from './pages/search/search.component';
65
import { ProductDetailsComponent } from './pages/product-details/product-details.component';
76
import { SearchCatalogComponent } from './pages/search-catalog/search-catalog.component';
87
import { CatalogsComponent } from './pages/catalogs/catalogs.component';
@@ -28,8 +27,10 @@ export const routes: Routes = [
2827
canActivate: [AuthGuard],
2928
data: { roles: [], is_isbe: environment.ISBE_CATALOGUE }
3029
},
31-
{ path: 'search', component: SearchComponent },
32-
{ path: 'search/:id', component: ProductDetailsComponent },
30+
{ path: 'search/:id', component: ProductDetailsComponent,
31+
canActivate: [AuthGuard],
32+
data: { roles: [], is_isbe: environment.ISBE_CATALOGUE }
33+
},
3334
{ path: 'org-details/:id', component: OrganizationDetailsComponent },
3435

3536
{ path: 'search/catalogue/:id', component: SearchCatalogComponent ,

src/app/offerings/gallery/gallery.component.html

Lines changed: 83 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,88 @@
11
<!-- Offerings Gallery section -->
2-
<h2 class="text-4xl mt-4 font-extrabold text-primary-100 text-center dark:text-primary-50">{{ 'GALLERY._title' | translate }}</h2>
3-
<div id="featuredOfferings" class="pt-8 pb-8 px-4 mx-auto max-w-screen-xl w-full grid gap-2 grid-cols-1 place-items-center sm:grid-cols-2 xl:grid-cols-4 lg:pb-16">
4-
@for (prod of products; track prod.id; let index = $index) {
5-
<bae-off-card id="featuredOfferCard" [productOff]=prod [cardId]="index" class="w-full h-full"></bae-off-card>
6-
} @empty {
2+
<bae-categories-panel class="fixed z-30 w-full top-[72px] transition transform opacity-0 duration-200" [ngClass]="showPanel ? 'opacity-100' : 'hidden'"></bae-categories-panel>
3+
<section class="flex p-5" [ngClass]="showPanel ? 'pt-[75px]' : ''">
4+
@if(showDrawer){
5+
<div (click)="$event.stopPropagation();" [ngClass]="showDrawer ? 'backdrop-blur-sm': ''" class="fixed h-screen w-3/4 top-0 left-0 z-50 p-4 overflow-y-auto bg-white dark:bg-gray-800" tabindex="-1" aria-labelledby="drawer-label">
6+
<button type="button" (click)="showDrawer=!showDrawer" aria-controls="drawer-example" class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 absolute top-2.5 end-2.5 flex items-center justify-center dark:hover:bg-gray-600 dark:hover:text-white" >
7+
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
8+
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"/>
9+
</svg>
10+
<span class="sr-only">{{ 'DASHBOARD._close_menu' | translate }}</span>
11+
</button>
12+
<bae-categories-filter [catalogId]="DFT_CATALOG"></bae-categories-filter>
13+
</div>
14+
} @else {
15+
<bae-categories-filter [catalogId]="DFT_CATALOG" class="hidden md:block w-1/5 md:w-2/4 lg:w-1/4"></bae-categories-filter>
716
}
8-
</div>
17+
18+
<div class="flex flex-col w-full md:w-4/5 lg:w-3/4">
19+
<section class="md:pl-5 content pb-5 px-4 flex items-center">
20+
<button (click)="showDrawer=!showDrawer;$event.stopPropagation();" type="button" class="md:hidden px-2 w-fit h-fit py-2 text-sm font-medium text-center inline-flex items-center dark:text-white bg-white text-primary-100 border border-primary-100 rounded-lg dark:bg-primary-100 dark:border-secondary-200">
21+
<svg class="w-4 h-4 mr-2" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 17 14">
22+
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1 1h15M1 7h15M1 13h15"/>
23+
</svg>
24+
Categories
25+
</button>
26+
@if(searchEnabled){
27+
<form class="mx-5 w-full">
28+
<div class="flex">
29+
<div class="relative w-full">
30+
<input type="search" id="search" [formControl]="searchField"
31+
(keydown.enter)="filterSearch($event)"
32+
class="block p-2.5 w-full text-sm text-gray-900 bg-gray-50 rounded-lg border border-gray-300 focus:ring-purple-500 focus:border-purple-500 dark:bg-gray-700 dark:border-s-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:border-purple-500"
33+
placeholder="{{ 'DASHBOARD._search_ph' | translate }}" required>
34+
<button type="submit" (click)="filterSearch($event)" class="absolute top-0 end-0 p-2.5 text-sm font-medium h-full text-white bg-purple-700 rounded-e-lg border border-purple-700 hover:bg-purple-800 focus:ring-4 focus:outline-none focus:ring-purple-300 dark:bg-purple-600 dark:hover:bg-purple-700 dark:focus:ring-purple-800">
35+
<svg class="w-4 h-4" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
36+
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z"/>
37+
</svg>
38+
<span class="sr-only">{{ 'DASHBOARD._search' | translate }}</span>
39+
</button>
40+
</div>
41+
</div>
42+
</form>
43+
}
44+
</section>
45+
@if (loading) {
46+
<div role="status" class=" h-full flex justify-center align-middle">
47+
<svg aria-hidden="true" class="w-12 h-12 text-gray-200 animate-spin dark:text-gray-600 fill-purple-600" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg">
48+
<path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="currentColor"/>
49+
<path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="currentFill"/>
50+
</svg>
51+
<span class="sr-only">Loading...</span>
52+
</div>
53+
} @else {
54+
<div class="md:pl-5 grid grid-cols-1 place-items-center lg:grid-cols-2 xl:grid-cols-3">
55+
@for (prod of products; track prod.id; let index = $index) {
56+
<bae-off-card [productOff]=prod [cardId]="index" class="w-full h-full p-2"></bae-off-card>
57+
} @empty {
58+
<div class="min-h-19 dark:text-gray-600 text-center">{{ 'DASHBOARD._not_found' | translate }}</div>
59+
}
60+
</div>
61+
@if (!loading_more) {
62+
@if (page_check) {
63+
<div class="flex pb-12 justify-center align-middle">
64+
<button (click)="next()" class="flex cursor-pointer items-center justify-center px-3 h-8 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-lg hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white">
65+
Load more
66+
<svg class="w-3.5 h-3.5 ms-2" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
67+
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14m-7 7V5"/>
68+
</svg>
69+
</button>
70+
</div>
71+
}
72+
} @else {
73+
<div role="status" class="w-full h-full flex justify-center align-middle">
74+
<svg aria-hidden="true" class="w-12 h-12 text-gray-200 animate-spin dark:text-gray-600 fill-purple-600" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg">
75+
<path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="currentColor"/>
76+
<path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="currentFill"/>
77+
</svg>
78+
<span class="sr-only">Loading...</span>
79+
</div>
80+
}
81+
}
82+
</div>
83+
84+
</section>
85+
986
@if (products.length === 0){
1087
<div class="py-8 flex justify-center w-full lg:py-16">
1188
<div class="flex items-center p-4 mb-4 text-sm text-primary-100 rounded-lg bg-purple-50 dark:bg-secondary-200 dark:text-primary-50" role="alert">

src/app/offerings/gallery/gallery.component.spec.ts

Lines changed: 77 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,96 @@ import { beforeEach, describe, expect, it } from '@jest/globals';
44
import { GalleryComponent } from './gallery.component';
55
import { HttpClientTestingModule } from '@angular/common/http/testing';
66
import { TranslateModule } from '@ngx-translate/core';
7+
import { BehaviorSubject, Subject } from 'rxjs';
8+
import { AuthService } from 'src/app/guard/auth.service';
9+
import { LocalStorageService } from 'src/app/services/local-storage.service';
10+
import { EventMessageService } from 'src/app/services/event-message.service';
11+
import { PaginationService } from 'src/app/services/pagination.service';
12+
import { ChangeDetectorRef } from '@angular/core';
13+
import { ActivatedRoute } from '@angular/router';
14+
import { MarkdownModule } from 'ngx-markdown';
715

816
describe('GalleryComponent', () => {
917
let component: GalleryComponent;
1018
let fixture: ComponentFixture<GalleryComponent>;
1119

20+
const authMock = { isAuthenticated$: new BehaviorSubject<boolean>(true) };
21+
const cdrMock = { detectChanges: jest.fn() };
22+
const routeMock = { snapshot: { paramMap: { get: jest.fn(() => null) } } } as unknown as Partial<ActivatedRoute>;
23+
const localStorageMock = { getObject: jest.fn(() => []), setItem: jest.fn() };
24+
const messages$ = new Subject<any>();
25+
const eventMessageMock = { messages$: messages$, emitFilterShown: jest.fn(), emitFilterShownCategory: jest.fn() } as Partial<EventMessageService>;
26+
const paginationServiceMock = {
27+
getItemsPaginated: jest.fn(() =>
28+
Promise.resolve({
29+
page_check: true,
30+
items: [{ id: '1', name: 'p1' }],
31+
nextItems: [],
32+
page: 0
33+
})
34+
),
35+
getProducts: jest.fn()
36+
} as Partial<PaginationService>;
37+
1238
beforeEach(async () => {
1339
await TestBed.configureTestingModule({
14-
imports: [GalleryComponent,HttpClientTestingModule, TranslateModule.forRoot()]
15-
})
16-
.compileComponents();
17-
40+
imports: [GalleryComponent, HttpClientTestingModule, TranslateModule.forRoot(), MarkdownModule.forRoot()],
41+
providers: [
42+
{ provide: AuthService, useValue: authMock },
43+
{ provide: ChangeDetectorRef, useValue: cdrMock },
44+
{ provide: ActivatedRoute, useValue: routeMock },
45+
{ provide: LocalStorageService, useValue: localStorageMock },
46+
{ provide: EventMessageService, useValue: eventMessageMock },
47+
{ provide: PaginationService, useValue: paginationServiceMock }
48+
]
49+
}).compileComponents();
50+
1851
fixture = TestBed.createComponent(GalleryComponent);
1952
component = fixture.componentInstance;
53+
jest.clearAllMocks();
2054
fixture.detectChanges();
2155
});
2256

2357
it('should create', () => {
2458
expect(component).toBeTruthy();
2559
});
60+
61+
it('should load products on init via paginationService and set products', async () => {
62+
await fixture.whenStable();
63+
expect(paginationServiceMock.getItemsPaginated).toHaveBeenCalled();
64+
expect(component.products).toEqual([{ id: '1', name: 'p1' }]);
65+
expect(component.page_check).toBe(true);
66+
expect(component.loading).toBe(false);
67+
});
68+
69+
it('checkPanel should emit and persist when filters present', () => {
70+
(localStorageMock.getObject as jest.Mock).mockReturnValue([{ id: 'cat1' }]);
71+
(eventMessageMock.emitFilterShown as jest.Mock).mockClear();
72+
component.showPanel = false;
73+
component.checkPanel();
74+
expect(eventMessageMock.emitFilterShown).toHaveBeenCalledWith(true);
75+
expect(localStorageMock.setItem).toHaveBeenCalledWith('is_filter_panel_shown', 'true');
76+
});
77+
78+
it('filterSearch should set keywords and call getProducts', async () => {
79+
const getProductsSpy = jest.spyOn(component, 'getProducts').mockResolvedValue(undefined);
80+
component.searchField.setValue('search-term');
81+
await component.filterSearch({ preventDefault: () => {} } as any);
82+
expect(component.keywords).toBe('search-term');
83+
expect(getProductsSpy).toHaveBeenCalledWith(false);
84+
getProductsSpy.mockRestore();
85+
});
86+
87+
it('filterSearch should clear keywords and call getProducts when empty', async () => {
88+
const getProductsSpy = jest.spyOn(component, 'getProducts').mockResolvedValue(undefined);
89+
component.searchField.setValue('');
90+
await component.filterSearch({ preventDefault: () => {} } as any);
91+
expect(component.keywords).toBeUndefined();
92+
expect(getProductsSpy).toHaveBeenCalledWith(false);
93+
getProductsSpy.mockRestore();
94+
});
95+
96+
afterAll(() => {
97+
messages$.complete();
98+
});
2699
});

0 commit comments

Comments
 (0)