diff --git a/.github/workflows/service-ci-build.yml b/.github/workflows/service-ci-build.yml index ef121541e..247061025 100644 --- a/.github/workflows/service-ci-build.yml +++ b/.github/workflows/service-ci-build.yml @@ -31,6 +31,8 @@ jobs: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} SONAR_PROJECT_KEY : ${{ secrets.SONAR_PROJECT_KEY }} steps: + - name: Remove unused sonar images + run: docker image prune -af - name: Set up JDK 17 uses: actions/setup-java@v3 with: diff --git a/.github/workflows/ui-ci-build.yml b/.github/workflows/ui-ci-build.yml index 5b1db345c..0802e839d 100644 --- a/.github/workflows/ui-ci-build.yml +++ b/.github/workflows/ui-ci-build.yml @@ -39,9 +39,8 @@ jobs: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} steps: - - name: Setup chrome - uses: browser-actions/setup-chrome@v1 - + - name: Remove unused sonar images + run: docker image prune -af - name: Execute Tests run: | cd ./marketplace-ui diff --git a/marketplace-service/src/main/java/com/axonivy/market/enums/ErrorCode.java b/marketplace-service/src/main/java/com/axonivy/market/enums/ErrorCode.java index c12d7ed44..fa939c58c 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/enums/ErrorCode.java +++ b/marketplace-service/src/main/java/com/axonivy/market/enums/ErrorCode.java @@ -18,7 +18,8 @@ public enum ErrorCode { GH_FILE_STATUS_INVALID("0201", "GIT_HUB_FILE_STATUS_INVALID"), GH_FILE_TYPE_INVALID("0202", "GIT_HUB_FILE_TYPE_INVALID"), USER_NOT_FOUND("2103", "USER_NOT_FOUND"), GITHUB_USER_NOT_FOUND("2204", "GITHUB_USER_NOT_FOUND"), GITHUB_USER_UNAUTHORIZED("2205", "GITHUB_USER_UNAUTHORIZED"), - FEEDBACK_NOT_FOUND("3103", "FEEDBACK_NOT_FOUND"), ARGUMENT_BAD_REQUEST("4000", "ARGUMENT_BAD_REQUEST"); + FEEDBACK_NOT_FOUND("3103", "FEEDBACK_NOT_FOUND"), NO_FEEDBACK_OF_USER_FOR_PRODUCT("3103", "NO_FEEDBACK_OF_USER_FOR_PRODUCT"), + ARGUMENT_BAD_REQUEST("4000", "ARGUMENT_BAD_REQUEST"); String code; String helpText; diff --git a/marketplace-service/src/main/java/com/axonivy/market/exceptions/ExceptionHandlers.java b/marketplace-service/src/main/java/com/axonivy/market/exceptions/ExceptionHandlers.java index 3ec95b44d..24738b39a 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/exceptions/ExceptionHandlers.java +++ b/marketplace-service/src/main/java/com/axonivy/market/exceptions/ExceptionHandlers.java @@ -3,6 +3,7 @@ import com.axonivy.market.enums.ErrorCode; import com.axonivy.market.exceptions.model.InvalidParamException; import com.axonivy.market.exceptions.model.MissingHeaderException; +import com.axonivy.market.exceptions.model.NoContentException; import com.axonivy.market.exceptions.model.NotFoundException; import com.axonivy.market.exceptions.model.Oauth2ExchangeCodeException; import com.axonivy.market.exceptions.model.UnauthorizedException; @@ -59,6 +60,14 @@ public ResponseEntity handleNotFoundException(NotFoundException notFound return new ResponseEntity<>(errorMessage, HttpStatus.NOT_FOUND); } + @ExceptionHandler(NoContentException.class) + public ResponseEntity handleNoContentException(NoContentException noContentException) { + var errorMessage = new Message(); + errorMessage.setHelpCode(noContentException.getCode()); + errorMessage.setMessageDetails(noContentException.getMessage()); + return new ResponseEntity<>(errorMessage, HttpStatus.NO_CONTENT); + } + @ExceptionHandler(InvalidParamException.class) public ResponseEntity handleInvalidException(InvalidParamException invalidDataException) { var errorMessage = new Message(); diff --git a/marketplace-service/src/main/java/com/axonivy/market/exceptions/model/NoContentException.java b/marketplace-service/src/main/java/com/axonivy/market/exceptions/model/NoContentException.java new file mode 100644 index 000000000..9d8a74fc6 --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/exceptions/model/NoContentException.java @@ -0,0 +1,27 @@ +package com.axonivy.market.exceptions.model; + +import com.axonivy.market.enums.ErrorCode; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +import java.io.Serial; + +@Getter +@Setter +@AllArgsConstructor +public class NoContentException extends RuntimeException { + + @Serial + private static final long serialVersionUID = 1L; + private static final String SEPARATOR = "-"; + + private final String code; + private final String message; + + public NoContentException(ErrorCode errorCode, String additionalMessage) { + this.code = errorCode.getCode(); + this.message = errorCode.getHelpText() + SEPARATOR + additionalMessage; + } + +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/impl/FeedbackServiceImpl.java b/marketplace-service/src/main/java/com/axonivy/market/service/impl/FeedbackServiceImpl.java index b4148918c..3d648c144 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/service/impl/FeedbackServiceImpl.java +++ b/marketplace-service/src/main/java/com/axonivy/market/service/impl/FeedbackServiceImpl.java @@ -2,6 +2,7 @@ import com.axonivy.market.entity.Feedback; import com.axonivy.market.enums.ErrorCode; +import com.axonivy.market.exceptions.model.NoContentException; import com.axonivy.market.exceptions.model.NotFoundException; import com.axonivy.market.model.FeedbackModelRequest; import com.axonivy.market.model.ProductRating; @@ -9,6 +10,7 @@ import com.axonivy.market.repository.ProductRepository; import com.axonivy.market.repository.UserRepository; import com.axonivy.market.service.FeedbackService; +import org.apache.commons.lang3.StringUtils; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; @@ -45,14 +47,16 @@ public Feedback findFeedback(String id) throws NotFoundException { } @Override - public Feedback findFeedbackByUserIdAndProductId(String userId, String productId) throws NotFoundException { - validateUserExists(userId); + public Feedback findFeedbackByUserIdAndProductId(String userId, String productId) throws NotFoundException, NoContentException { + if (StringUtils.isNotBlank(userId)) { + validateUserExists(userId); + } validateProductExists(productId); Feedback existingUserFeedback = feedbackRepository.findByUserIdAndProductId(userId, productId); if (existingUserFeedback == null) { - throw new NotFoundException(ErrorCode.FEEDBACK_NOT_FOUND, - String.format("Not found feedback with user id '%s' and product id '%s'", userId, productId)); + throw new NoContentException(ErrorCode.NO_FEEDBACK_OF_USER_FOR_PRODUCT, + String.format("No feedback with user id '%s' and product id '%s'", userId, productId)); } return existingUserFeedback; } diff --git a/marketplace-service/src/test/java/com/axonivy/market/handler/ExceptionHandlersTest.java b/marketplace-service/src/test/java/com/axonivy/market/handler/ExceptionHandlersTest.java index ad5db1617..b5b16ed94 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/handler/ExceptionHandlersTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/handler/ExceptionHandlersTest.java @@ -3,6 +3,7 @@ import com.axonivy.market.exceptions.ExceptionHandlers; import com.axonivy.market.exceptions.model.InvalidParamException; import com.axonivy.market.exceptions.model.MissingHeaderException; +import com.axonivy.market.exceptions.model.NoContentException; import com.axonivy.market.exceptions.model.NotFoundException; import com.axonivy.market.model.Message; import org.junit.jupiter.api.BeforeEach; @@ -47,6 +48,13 @@ void testHandleNotFoundException() { assertEquals(HttpStatus.NOT_FOUND, responseEntity.getStatusCode()); } + @Test + void testHandleNoContentException() { + var noContentException = mock(NoContentException.class); + var responseEntity = exceptionHandlers.handleNoContentException(noContentException); + assertEquals(HttpStatus.NO_CONTENT, responseEntity.getStatusCode()); + } + @Test void testHandleInvalidException() { var invalidParamException = mock(InvalidParamException.class); diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/impl/FeedbackServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/impl/FeedbackServiceImplTest.java index b2f6cfded..45be07c10 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/service/impl/FeedbackServiceImplTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/service/impl/FeedbackServiceImplTest.java @@ -4,6 +4,7 @@ import com.axonivy.market.entity.Product; import com.axonivy.market.entity.User; import com.axonivy.market.enums.ErrorCode; +import com.axonivy.market.exceptions.model.NoContentException; import com.axonivy.market.exceptions.model.NotFoundException; import com.axonivy.market.model.FeedbackModel; import com.axonivy.market.model.FeedbackModelRequest; @@ -133,7 +134,7 @@ void testFindFeedback_NotFound() { } @Test - void testFindFeedbackByUserIdAndProductId() throws NotFoundException { + void testFindFeedbackByUserIdAndProductId() { String productId = "product1"; when(userRepository.findById(userId)).thenReturn(Optional.of(new User())); @@ -150,21 +151,30 @@ void testFindFeedbackByUserIdAndProductId() throws NotFoundException { } @Test - void testFindFeedbackByUserIdAndProductId_NotFound() { + void testFindFeedbackByUserIdAndProductId_NoContent() { String productId = "product1"; - - when(userRepository.findById(userId)).thenReturn(Optional.of(new User())); + userId = ""; when(productRepository.findById(productId)).thenReturn(Optional.of(new Product())); when(feedbackRepository.findByUserIdAndProductId(userId, productId)).thenReturn(null); - NotFoundException exception = assertThrows(NotFoundException.class, + NoContentException exception = assertThrows(NoContentException.class, () -> feedbackService.findFeedbackByUserIdAndProductId(userId, productId)); - assertEquals(ErrorCode.FEEDBACK_NOT_FOUND.getCode(), exception.getCode()); - verify(userRepository, times(1)).findById(userId); + assertEquals(ErrorCode.NO_FEEDBACK_OF_USER_FOR_PRODUCT.getCode(), exception.getCode()); verify(productRepository, times(1)).findById(productId); verify(feedbackRepository, times(1)).findByUserIdAndProductId(userId, productId); } + @Test + void testFindFeedbackByUserIdAndProductId_NotFound() { + userId = "notFoundUser"; + + when(userRepository.findById(userId)).thenReturn(Optional.empty()); + NotFoundException exception = assertThrows(NotFoundException.class, + () -> feedbackService.findFeedbackByUserIdAndProductId(userId, "product")); + assertEquals(ErrorCode.USER_NOT_FOUND.getCode(), exception.getCode()); + verify(userRepository, times(1)).findById(userId); + } + @Test void testUpsertFeedback_Insert() throws NotFoundException { String productId = "product1"; diff --git a/marketplace-ui/src/app/app.component.spec.ts b/marketplace-ui/src/app/app.component.spec.ts index 2a9174723..a2bdd7a12 100644 --- a/marketplace-ui/src/app/app.component.spec.ts +++ b/marketplace-ui/src/app/app.component.spec.ts @@ -4,9 +4,10 @@ import { FooterComponent } from './shared/components/footer/footer.component'; import { HeaderComponent } from './shared/components/header/header.component'; import { LoadingService } from './core/services/loading/loading.service'; import { RoutingQueryParamService } from './shared/services/routing.query.param.service'; -import { ActivatedRoute, RouterOutlet, NavigationStart } from '@angular/router'; +import { ActivatedRoute, RouterOutlet, NavigationStart, RouterModule, Router, NavigationError, Event } from '@angular/router'; import { of, Subject } from 'rxjs'; import { TranslateService, TranslateModule } from '@ngx-translate/core'; +import { ERROR_PAGE_PATH } from './shared/constants/common.constant'; describe('AppComponent', () => { let component: AppComponent; @@ -14,9 +15,12 @@ describe('AppComponent', () => { let routingQueryParamService: jasmine.SpyObj; let activatedRoute: ActivatedRoute; let navigationStartSubject: Subject; + let router: Router; + let routerEventsSubject: Subject; beforeEach(async () => { navigationStartSubject = new Subject(); + routerEventsSubject = new Subject(); const loadingServiceSpy = jasmine.createSpyObj('LoadingService', [ 'isLoading' ]); @@ -30,13 +34,19 @@ describe('AppComponent', () => { ] ); + const routerMock = { + events: routerEventsSubject.asObservable(), + navigate: jasmine.createSpy('navigate'), + }; + await TestBed.configureTestingModule({ imports: [ AppComponent, RouterOutlet, HeaderComponent, FooterComponent, - TranslateModule.forRoot() + TranslateModule.forRoot(), + RouterModule.forRoot([]) ], providers: [ { provide: LoadingService, useValue: loadingServiceSpy }, @@ -50,7 +60,8 @@ describe('AppComponent', () => { queryParams: of({}) } }, - TranslateService + TranslateService, + { provide: Router, useValue: routerMock } ] }).compileComponents(); @@ -59,11 +70,13 @@ describe('AppComponent', () => { routingQueryParamService = TestBed.inject( RoutingQueryParamService ) as jasmine.SpyObj; - activatedRoute = TestBed.inject(ActivatedRoute); routingQueryParamService.getNavigationStartEvent.and.returnValue( navigationStartSubject.asObservable() ); + activatedRoute = TestBed.inject(ActivatedRoute); + router = TestBed.inject(Router); + fixture.detectChanges(); }); it('should create the app', () => { @@ -104,4 +117,11 @@ describe('AppComponent', () => { routingQueryParamService.checkCookieForDesignerVersion ).not.toHaveBeenCalled(); }); + + it('should redirect to "/error-page" on NavigationError', () => { + // Simulate a NavigationError event + const navigationError = new NavigationError(1, '/a-trust/test-url', 'Error message'); + routerEventsSubject.next(navigationError); + expect(router.navigate).toHaveBeenCalledWith([ERROR_PAGE_PATH]); + }); }); diff --git a/marketplace-ui/src/app/app.component.ts b/marketplace-ui/src/app/app.component.ts index 25ab92bc5..4fd4ae623 100644 --- a/marketplace-ui/src/app/app.component.ts +++ b/marketplace-ui/src/app/app.component.ts @@ -1,9 +1,10 @@ import { Component, inject } from '@angular/core'; -import { RouterOutlet, ActivatedRoute } from '@angular/router'; +import { RouterOutlet, ActivatedRoute, Router, NavigationError, Event } from '@angular/router'; import { FooterComponent } from './shared/components/footer/footer.component'; import { HeaderComponent } from './shared/components/header/header.component'; import { LoadingService } from './core/services/loading/loading.service'; import { RoutingQueryParamService } from './shared/services/routing.query.param.service'; +import { ERROR_PAGE_PATH } from './shared/constants/common.constant'; @Component({ selector: 'app-root', @@ -17,9 +18,15 @@ export class AppComponent { routingQueryParamService = inject(RoutingQueryParamService); route = inject(ActivatedRoute); - constructor() {} + constructor(private readonly router: Router) {} ngOnInit(): void { + this.router.events.subscribe((event: Event) => { + if (event instanceof NavigationError) { + this.router.navigate([ERROR_PAGE_PATH]); + } + }); + this.routingQueryParamService.getNavigationStartEvent().subscribe(() => { if (!this.routingQueryParamService.isDesignerEnv()) { this.route.queryParams.subscribe(params => { diff --git a/marketplace-ui/src/app/app.routes.ts b/marketplace-ui/src/app/app.routes.ts index 76ebff774..08f5524ec 100644 --- a/marketplace-ui/src/app/app.routes.ts +++ b/marketplace-ui/src/app/app.routes.ts @@ -1,7 +1,12 @@ import { Routes } from '@angular/router'; import { GithubCallbackComponent } from './auth/github-callback/github-callback.component'; +import { ErrorPageComponentComponent } from './shared/components/error-page-component/error-page-component.component'; export const routes: Routes = [ + { + path: 'error-page', + component: ErrorPageComponentComponent + }, { path: '', loadChildren: () => import('./modules/home/home.routes').then(m => m.routes) diff --git a/marketplace-ui/src/app/core/interceptors/api.interceptor.spec.ts b/marketplace-ui/src/app/core/interceptors/api.interceptor.spec.ts new file mode 100644 index 000000000..6870339a5 --- /dev/null +++ b/marketplace-ui/src/app/core/interceptors/api.interceptor.spec.ts @@ -0,0 +1,66 @@ +import { HttpClient, HttpHeaders, provideHttpClient, withInterceptors } from '@angular/common/http'; +import { HttpTestingController } from '@angular/common/http/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ActivatedRoute } from '@angular/router'; +import { TranslateModule } from '@ngx-translate/core'; +import { of } from 'rxjs'; +import { ProductComponent } from '../../modules/product/product.component'; +import { DESIGNER_COOKIE_VARIABLE } from '../../shared/constants/common.constant'; +import { apiInterceptor } from './api.interceptor'; + +describe('AuthInterceptor', () => { + let productComponent: ProductComponent; + let fixture: ComponentFixture; + let httpClient: HttpClient; + let httpTestingController: HttpTestingController; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ProductComponent, TranslateModule.forRoot()], + providers: [ + provideHttpClient(withInterceptors([apiInterceptor])), + HttpTestingController, + { + provide: ActivatedRoute, + useValue: { + queryParams: of({ + [DESIGNER_COOKIE_VARIABLE.restClientParamName]: true + }) + } + } + ] + }); + + httpClient = TestBed.inject(HttpClient); + httpTestingController = TestBed.inject(HttpTestingController); + + fixture = TestBed.createComponent(ProductComponent); + productComponent = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should throw error', () => { + const headers = new HttpHeaders({ + 'X-Requested-By': 'ivy' + }); + httpClient.get('product', { headers }).subscribe({ + next() { + fail('Expected an error, but got a response'); + }, + error(e) { + expect(e.status).not.toBe(200); + } + }); + }); + + it('should throw error with the url contains i18n', () => { + httpClient.get('assets/i18n').subscribe({ + next() { + fail('Expected an error, but got a response'); + }, + error(e) { + expect(e.status).not.toBe(200); + } + }); + }); +}); diff --git a/marketplace-ui/src/app/core/interceptors/api.interceptor.ts b/marketplace-ui/src/app/core/interceptors/api.interceptor.ts index e8ec8fa2b..822ba7918 100644 --- a/marketplace-ui/src/app/core/interceptors/api.interceptor.ts +++ b/marketplace-ui/src/app/core/interceptors/api.interceptor.ts @@ -6,7 +6,9 @@ import { import { environment } from '../../../environments/environment'; import { LoadingService } from '../services/loading/loading.service'; import { inject } from '@angular/core'; -import { finalize } from 'rxjs'; +import { catchError, finalize, throwError } from 'rxjs'; +import { Router } from '@angular/router'; +import { ERROR_CODES, ERROR_PAGE_PATH } from '../../shared/constants/common.constant'; export const REQUEST_BY = 'X-Requested-By'; export const IVY = 'ivy'; @@ -17,6 +19,7 @@ export const IVY = 'ivy'; export const SkipLoading = new HttpContextToken(() => false); export const apiInterceptor: HttpInterceptorFn = (req, next) => { + const router = inject(Router); const loadingService = inject(LoadingService); if (req.url.includes('i18n')) { @@ -40,6 +43,12 @@ export const apiInterceptor: HttpInterceptorFn = (req, next) => { loadingService.show(); return next(cloneReq).pipe( + catchError(error => { + if (ERROR_CODES.includes(error.status)) { + router.navigate([ERROR_PAGE_PATH]); + } + return throwError(() => new Error(error.message)); + }), finalize(() => { loadingService.hide(); }) diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-information-tab/product-detail-information-tab.component.html b/marketplace-ui/src/app/modules/product/product-detail/product-detail-information-tab/product-detail-information-tab.component.html index c7e919360..a32508c3d 100644 --- a/marketplace-ui/src/app/modules/product/product-detail/product-detail-information-tab/product-detail-information-tab.component.html +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-information-tab/product-detail-information-tab.component.html @@ -50,13 +50,6 @@

{{ productDetail.type }}
-
- - {{ 'common.product.detail.information.value.industry' | translate }} - - {{ productDetail.industry }} -
-
{{ 'common.product.detail.information.value.tag' | translate }} diff --git a/marketplace-ui/src/app/shared/components/error-page-component/error-page-component.component.html b/marketplace-ui/src/app/shared/components/error-page-component/error-page-component.component.html new file mode 100644 index 000000000..9de60b3d1 --- /dev/null +++ b/marketplace-ui/src/app/shared/components/error-page-component/error-page-component.component.html @@ -0,0 +1,27 @@ +
+
+
{{ 'common.error.code' | translate }}
+
{{ 'common.error.oops' | translate }}
+
+ {{ 'common.error.description' | translate }} +
+
+ +
+
+
+
+ 404 Not Found +
+
diff --git a/marketplace-ui/src/app/shared/components/error-page-component/error-page-component.component.scss b/marketplace-ui/src/app/shared/components/error-page-component/error-page-component.component.scss new file mode 100644 index 000000000..0085d3d45 --- /dev/null +++ b/marketplace-ui/src/app/shared/components/error-page-component/error-page-component.component.scss @@ -0,0 +1,54 @@ +.error-page-container { + margin-top: 5rem; + &.mobile-mode { + .error-code-404, .oops, .description, .button-container { + text-align: center; + } + + .error-description { + text-align: center; + } + } + + .error-code-404 { + font-size: 14px; + font-weight: 600; + line-height: 16.8px; + letter-spacing: 0.18em; + text-align: left; + color: var(--text-error-code-color); + } + + .oops { + font-size: 72px; + font-weight: 600; + line-height: 79.2px; + text-align: left; + color: var(--text-error-oops-color); + } + + .description { + font-size: 18px; + font-weight: 400; + line-height: 25.2px; + text-align: left; + } + + .flexible-gap, + .module-gap { + gap: 1.5rem; + } + + .button-container { + margin-bottom: 1.5rem; + } + + .error-description { + color: var(--ivy-text-secondary-color); + font-family: Inter; + font-size: 18px; + font-weight: 400; + line-height: 25.2px; + text-align: left; + } +} diff --git a/marketplace-ui/src/app/shared/components/error-page-component/error-page-component.component.spec.ts b/marketplace-ui/src/app/shared/components/error-page-component/error-page-component.component.spec.ts new file mode 100644 index 000000000..a1d35370e --- /dev/null +++ b/marketplace-ui/src/app/shared/components/error-page-component/error-page-component.component.spec.ts @@ -0,0 +1,82 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ErrorPageComponentComponent } from './error-page-component.component'; +import { TranslateModule } from '@ngx-translate/core'; +import { Viewport } from 'karma-viewport/dist/adapter/viewport'; +import { By } from '@angular/platform-browser'; +import { Router } from '@angular/router'; +declare const viewport: Viewport; + +describe('ErrorPageComponentComponent', () => { + let component: ErrorPageComponentComponent; + let fixture: ComponentFixture; + let router: Router; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ErrorPageComponentComponent, TranslateModule.forRoot()] + }).compileComponents(); + + fixture = TestBed.createComponent(ErrorPageComponentComponent); + component = fixture.componentInstance; + router = TestBed.inject(Router); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should call checkMediaSize on window resize', () => { + spyOn(component, 'checkMediaSize'); + component.onResize(); + expect(component.checkMediaSize).toHaveBeenCalled(); + }); + + it('should display image with the light mode on small and large viewport', () => { + component.themeService.isDarkMode.set(false); + viewport.set(1920); + component.onResize(); + fixture.detectChanges(); + let imgElement = fixture.debugElement.query(By.css('img')); + expect(imgElement.attributes['src']).toBe('/assets/images/misc/robot.png'); + + viewport.set(540); + component.onResize(); + fixture.detectChanges(); + expect(imgElement.attributes['src']).toBe( + '/assets/images/misc/robot-mobile.png' + ); + }); + + it('should display image with the dark mode on small and large viewport', () => { + component.themeService.isDarkMode.set(true); + viewport.set(1920); + component.onResize(); + fixture.detectChanges(); + let imgElement = fixture.debugElement.query(By.css('img')); + expect(imgElement.attributes['src']).toBe( + '/assets/images/misc/robot-black.png' + ); + + viewport.set(540); + component.onResize(); + fixture.detectChanges(); + expect(imgElement.attributes['src']).toBe( + '/assets/images/misc/robot-mobile-black.png' + ); + }); + + it('should back to the home page', () => { + spyOn(component, 'backToHomePage'); + let buttonElement = fixture.debugElement.query(By.css('button')); + buttonElement.nativeElement.click(); + expect(component.backToHomePage).toHaveBeenCalled(); + }); + + it('should redirect to the home page', () => { + const navigateSpy = spyOn(router, 'navigate'); + component.backToHomePage(); + expect(navigateSpy).toHaveBeenCalledWith(['/']); + }); +}); diff --git a/marketplace-ui/src/app/shared/components/error-page-component/error-page-component.component.ts b/marketplace-ui/src/app/shared/components/error-page-component/error-page-component.component.ts new file mode 100644 index 000000000..f97495db8 --- /dev/null +++ b/marketplace-ui/src/app/shared/components/error-page-component/error-page-component.component.ts @@ -0,0 +1,53 @@ +import { Component, HostListener, inject, signal } from '@angular/core'; +import { ThemeService } from '../../../core/services/theme/theme.service'; +import { LanguageService } from '../../../core/services/language/language.service'; +import { CommonModule } from '@angular/common'; +import { TranslateModule } from '@ngx-translate/core'; +import { Router } from '@angular/router'; + +@Component({ + selector: 'app-error-page-component', + standalone: true, + imports: [CommonModule, TranslateModule], + templateUrl: './error-page-component.component.html', + styleUrl: './error-page-component.component.scss' +}) +export class ErrorPageComponentComponent { + themeService = inject(ThemeService); + languageService = inject(LanguageService); + isMobileMode = signal(false); + + constructor(private readonly router: Router) { + this.checkMediaSize(); + } + + backToHomePage() { + this.router.navigate(['/']); + } + + @HostListener('window:resize', ['$event']) + onResize() { + this.checkMediaSize(); + } + + checkMediaSize() { + const mediaQuery = window.matchMedia('(max-width: 767px)'); + this.isMobileMode.set(mediaQuery.matches); + } + + getImageSrcInLightMode(): string { + if (this.isMobileMode()) { + return '/assets/images/misc/robot-mobile.png'; + } + + return '/assets/images/misc/robot.png'; + } + + getImageSrcInDarkMode(): string { + if (this.isMobileMode()) { + return '/assets/images/misc/robot-mobile-black.png'; + } + + return '/assets/images/misc/robot-black.png'; + } +} diff --git a/marketplace-ui/src/app/shared/constants/common.constant.ts b/marketplace-ui/src/app/shared/constants/common.constant.ts index c37d19e69..ac0d39c6b 100644 --- a/marketplace-ui/src/app/shared/constants/common.constant.ts +++ b/marketplace-ui/src/app/shared/constants/common.constant.ts @@ -195,3 +195,13 @@ export const VERSION = { tagPrefix: 'v', displayPrefix: 'Version ' }; + +export const ERROR_PAGE_PATH = '/error-page'; +export const NOT_FOUND_ERROR_CODE = 404; +export const INTERNAL_SERVER_ERROR_CODE = 500; +export const UNDEFINED_ERROR_CODE = 0; +export const ERROR_CODES = [ + UNDEFINED_ERROR_CODE, + NOT_FOUND_ERROR_CODE, + INTERNAL_SERVER_ERROR_CODE +]; \ No newline at end of file diff --git a/marketplace-ui/src/assets/i18n/de.yaml b/marketplace-ui/src/assets/i18n/de.yaml index bdfb6105c..c5780b0bf 100644 --- a/marketplace-ui/src/assets/i18n/de.yaml +++ b/marketplace-ui/src/assets/i18n/de.yaml @@ -23,6 +23,11 @@ common: util: Nützliches demos: Demos solution: Lösungen + error: + code: 'ERROR CODE: 404' + oops: 'Oops!' + description: 'Unsere Webseite macht eine kleine Pause. Offenbar finden wir die von Dir angefragte Seite nicht.' + buttonLabel: Zurück zur Startseite sort: label: Sortierung value: @@ -67,7 +72,6 @@ common: compatibility: Kompatibilität cost: Kosten language: Sprache - industry: Branche tag: Tags source: Quelle status: Status diff --git a/marketplace-ui/src/assets/i18n/en.yaml b/marketplace-ui/src/assets/i18n/en.yaml index dfe1cba41..46e51de55 100644 --- a/marketplace-ui/src/assets/i18n/en.yaml +++ b/marketplace-ui/src/assets/i18n/en.yaml @@ -27,6 +27,11 @@ common: util: Utilities demos: Demos solution: Solutions + error: + code: 'ERROR CODE: 404' + oops: 'Oops!' + description: 'Our website is taking a break. It seems that we cannot find the page you requested.' + buttonLabel: Back to homepage sort: label: Sort by value: @@ -71,7 +76,6 @@ common: compatibility: Compatibility cost: Cost language: Language - industry: Industry tag: Tags source: Source status: Status diff --git a/marketplace-ui/src/assets/images/misc/robot-black.png b/marketplace-ui/src/assets/images/misc/robot-black.png new file mode 100644 index 000000000..409970b56 Binary files /dev/null and b/marketplace-ui/src/assets/images/misc/robot-black.png differ diff --git a/marketplace-ui/src/assets/images/misc/robot-mobile-black.png b/marketplace-ui/src/assets/images/misc/robot-mobile-black.png new file mode 100644 index 000000000..b28ea8d63 Binary files /dev/null and b/marketplace-ui/src/assets/images/misc/robot-mobile-black.png differ diff --git a/marketplace-ui/src/assets/images/misc/robot-mobile.png b/marketplace-ui/src/assets/images/misc/robot-mobile.png new file mode 100644 index 000000000..3d81d117b Binary files /dev/null and b/marketplace-ui/src/assets/images/misc/robot-mobile.png differ diff --git a/marketplace-ui/src/assets/images/misc/robot.png b/marketplace-ui/src/assets/images/misc/robot.png new file mode 100644 index 000000000..66279ab7b Binary files /dev/null and b/marketplace-ui/src/assets/images/misc/robot.png differ diff --git a/marketplace-ui/src/assets/scss/custom-style.scss b/marketplace-ui/src/assets/scss/custom-style.scss index b9a1c1633..c1d859f50 100644 --- a/marketplace-ui/src/assets/scss/custom-style.scss +++ b/marketplace-ui/src/assets/scss/custom-style.scss @@ -78,6 +78,8 @@ p { --star-color: #b0b0b0; --star-filled-color: #{$ivyPrimaryTextColorLight}; --text-no-rating-color: #757575; + --text-error-code-color: #757575; + --text-error-oops-color: #{$ivyPrimaryTextColorLight}; .bg-primary { background-color: #{$ivyPrimaryColorLight} !important; @@ -180,6 +182,9 @@ p { --star-filled-color: #{$white}; --text-no-rating-color: #A3A3A3; + --text-error-code-color: #A3A3A3; + --text-error-oops-color: #ffffff; + a { color: #{$white}; }