From 8325bc9fcfcd4c1782fb29d4395175f9b6580049 Mon Sep 17 00:00:00 2001 From: Ivan Mitev <> Date: Tue, 21 Apr 2026 21:44:46 +0200 Subject: [PATCH 1/6] Category and browsing changes --- src/app/app-routing.module.ts | 5 + src/app/data/availableFilters.ts | 14 +- src/app/data/categoryIcons.ts | 29 + src/app/pages/browse/browse.component.css | 194 +++ src/app/pages/browse/browse.component.html | 172 +++ src/app/pages/browse/browse.component.ts | 197 +++ .../product-details.component.html | 1157 ++++++++--------- .../product-details.component.ts | 232 +--- src/app/pages/search/search.component.css | 7 + src/app/pages/search/search.component.html | 690 +++++++--- src/app/pages/search/search.component.ts | 431 +++++- src/app/services/pagination.service.ts | 6 +- src/app/services/product-service.service.ts | 10 +- src/app/shared/card/card.component.html | 182 ++- src/app/shared/card/card.component.ts | 43 +- .../categories-filter.component.html | 117 +- .../categories-filter.component.ts | 42 +- src/app/themes/dome.theme.ts | 14 +- src/assets/i18n/en.json | 13 + src/assets/i18n/es.json | 13 + src/assets/images/bg_1_shadow.png | Bin 1321078 -> 0 bytes 21 files changed, 2388 insertions(+), 1180 deletions(-) create mode 100644 src/app/data/categoryIcons.ts create mode 100644 src/app/pages/browse/browse.component.css create mode 100644 src/app/pages/browse/browse.component.html create mode 100644 src/app/pages/browse/browse.component.ts delete mode 100644 src/assets/images/bg_1_shadow.png diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index e81c2c53..34d3d426 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -138,6 +138,11 @@ const routes: Routes = [ canActivate: [AuthGuard], data: { roles: ['admin'] } }, + { + path: 'browse', + loadComponent: () => import('./pages/browse/browse.component').then(c => c.BrowseComponent), + }, + { path: 'landing-page', children: [{ diff --git a/src/app/data/availableFilters.ts b/src/app/data/availableFilters.ts index 2ab7f4ec..852f932c 100644 --- a/src/app/data/availableFilters.ts +++ b/src/app/data/availableFilters.ts @@ -1,17 +1,29 @@ export type Filter = { name: string + label?: string + clientSide?: boolean children?: Filter[] } -const availableFilters: Filter[] = [ +export const availableFilters: Filter[] = [ { name: 'compliance_profile', + label: 'Compliance level', children: [ { name: 'Baseline' }, { name: 'Professional' }, { name: 'Professional+' }, ], }, + { + name: 'procurement_type', + label: 'Procurement type', + clientSide: true, + children: [ + { name: 'Ready to Buy' }, + { name: 'Request Quote' }, + ], + }, ] export default availableFilters diff --git a/src/app/data/categoryIcons.ts b/src/app/data/categoryIcons.ts new file mode 100644 index 00000000..36d62725 --- /dev/null +++ b/src/app/data/categoryIcons.ts @@ -0,0 +1,29 @@ +import { IconDefinition } from '@fortawesome/fontawesome-svg-core'; +import { + faBolt, + faBrain, + faCode, + faLayerGroup, + faPlug, + faServer, + faShieldHalved, + faUserTie +} from '@fortawesome/pro-solid-svg-icons'; + +export const DEFAULT_CATEGORY_ICON: IconDefinition = faLayerGroup; + +export const CATEGORY_ICONS: Record = { + Security: faShieldHalved, + Infrastructure: faServer, + Productivity: faBolt, + DevOps: faCode, + Professional: faUserTie, + Specific: faLayerGroup, + 'Data and AI': faBrain, + Integration: faPlug, +}; + +export function iconForCategory(name: string | null | undefined): IconDefinition { + if (!name) return DEFAULT_CATEGORY_ICON; + return CATEGORY_ICONS[name] ?? DEFAULT_CATEGORY_ICON; +} diff --git a/src/app/pages/browse/browse.component.css b/src/app/pages/browse/browse.component.css new file mode 100644 index 00000000..9809f3b1 --- /dev/null +++ b/src/app/pages/browse/browse.component.css @@ -0,0 +1,194 @@ +.browse-wrap { + position: relative; + width: 100%; + isolation: isolate; +} + +.browse-bg { + position: absolute; + inset: 0; + z-index: 0; + overflow: hidden; + pointer-events: none; + background: #f3f6ff; +} + +.browse-content { + position: relative; + z-index: 1; +} + +.browse-ellipse { + position: absolute; + width: 720px; + height: 720px; + border-radius: 9999px; + transform: rotate(180deg); + opacity: 1; + background: radial-gradient(circle at 35% 35%, + rgba(182, 202, 236, 0.95) 0%, + rgba(182, 202, 236, 0.65) 38%, + rgba(182, 202, 236, 0) 72%); + filter: blur(10px); +} + +.browse-e1 { + left: -220px; + top: -190px; +} + +.browse-e2 { + right: -260px; + top: 60px; +} + +.browse-e3 { + left: 50%; + top: -240px; + transform: translateX(-50%) rotate(180deg); +} + +.browse-e4 { + left: 140px; + bottom: -360px; +} + +.browse-blur { + position: absolute; + inset: 0; + background: linear-gradient(226.59deg, + rgba(255, 242, 242, 0.18) 25.7%, + rgba(255, 250, 250, 0.28) 94.73%); + backdrop-filter: blur(90px); + -webkit-backdrop-filter: blur(90px); + transform: rotate(180deg); +} + +@media (max-width: 1024px) { + .browse-ellipse { + width: 620px; + height: 620px; + filter: blur(9px); + } + + .browse-e2 { + right: -300px; + } +} + +.popular-card { + transition: border-color 0.2s ease, box-shadow 0.2s ease; +} + +.popular-card:hover { + border-color: #3d71cc; + box-shadow: 0 6px 20px rgba(45, 88, 167, 0.12); +} + +.popular-grid[data-direction="init"], +.popular-grid[data-direction="next"] { + animation: popularSlideInRight 0.6s cubic-bezier(0.22, 1, 0.36, 1) both; +} + +.popular-grid[data-direction="prev"] { + animation: popularSlideInLeft 0.6s cubic-bezier(0.22, 1, 0.36, 1) both; +} + +@keyframes popularSlideInRight { + from { + transform: translateX(100%); + } + to { + transform: translateX(0); + } +} + +@keyframes popularSlideInLeft { + from { + transform: translateX(-100%); + } + to { + transform: translateX(0); + } +} + +.popular-arrow { + position: absolute; + top: 50%; + transform: translateY(-50%); + z-index: 2; + display: flex; + align-items: center; + justify-content: center; + width: 44px; + height: 44px; + border-radius: 9999px; + background: #ffffff; + color: #0b1220; + box-shadow: 0 4px 16px rgba(45, 88, 167, 0.2); + transition: color 0.2s ease, box-shadow 0.2s ease, opacity 0.2s ease; +} + +.popular-arrow:hover:not(:disabled) { + color: #1f4fbf; + box-shadow: 0 4px 18px rgba(45, 88, 167, 0.32); +} + +.popular-arrow:disabled { + opacity: 0.4; + cursor: not-allowed; +} + +.popular-arrow-left { + left: 0; +} + +.popular-arrow-right { + right: 0; +} + +.popular-dot { + width: 10px; + height: 10px; + border-radius: 9999px; + background: #d8dade; + transition: background 0.2s ease, width 0.2s ease; +} + +.popular-dot:hover { + background: #b8babf; +} + +.popular-dot-active { + background: #3d71cc; + width: 28px; +} + +@media (max-width: 640px) { + .popular-arrow { + display: none; + } +} + +@media (max-width: 640px) { + .browse-ellipse { + width: 520px; + height: 520px; + filter: blur(8px); + } + + .browse-e1 { + left: -280px; + top: -230px; + } + + .browse-e2 { + right: -320px; + top: 10px; + } + + .browse-e4 { + left: 40px; + bottom: -380px; + } +} diff --git a/src/app/pages/browse/browse.component.html b/src/app/pages/browse/browse.component.html new file mode 100644 index 00000000..5036a39f --- /dev/null +++ b/src/app/pages/browse/browse.component.html @@ -0,0 +1,172 @@ +
+ + +
+ +
+

+ {{ 'BROWSE.heroTitle1' | translate }} + {{ 'BROWSE.heroAccent1' | translate }} + {{ 'BROWSE.heroTitle2' | translate }} + {{ 'BROWSE.heroAccent2' | translate }} +

+ +
+
+ + +
+
+
+ +
+
+

+ {{ 'BROWSE.categoriesTitle' | translate }} +

+

+ {{ 'BROWSE.categoriesSubtitle' | translate }} +

+
+ + @if (loading) { +
+ + + + +
+ } @else { +
+ @for (category of categories; track category.id) { + + } + + +
+ } +
+ + @if (popularLoading || popularOffers.length > 0) { +
+
+

+ {{ 'BROWSE.popularTitle' | translate }} +

+

+ {{ 'BROWSE.popularSubtitle' | translate }} +

+
+ + @if (popularLoading) { +
+ + + + +
+ } @else { +
+ @if (popularPageCount > 1) { + + } + +
+ @for (page of [popularPage]; track page) { + + } +
+ + @if (popularPageCount > 1) { + + } +
+ + @if (popularPageCount > 1) { +
+ @for (_ of popularPages; track $index) { + + } +
+ } + } +
+ } + +
+
diff --git a/src/app/pages/browse/browse.component.ts b/src/app/pages/browse/browse.component.ts new file mode 100644 index 00000000..eabdb6d2 --- /dev/null +++ b/src/app/pages/browse/browse.component.ts @@ -0,0 +1,197 @@ +import { Component, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { Router } from '@angular/router'; +import { FormsModule } from '@angular/forms'; +import { TranslateModule } from '@ngx-translate/core'; +import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; +import { faArrowRight, faChevronLeft, faChevronRight } from '@fortawesome/pro-solid-svg-icons'; +import { ApiServiceService } from 'src/app/services/product-service.service'; +import { LocalStorageService } from 'src/app/services/local-storage.service'; +import { Category } from 'src/app/models/interfaces'; +import { iconForCategory } from 'src/app/data/categoryIcons'; + +interface PopularOffer { + id: string; + name: string; + description: string; + imageUrl: string; +} + +@Component({ + selector: 'app-browse', + standalone: true, + imports: [CommonModule, FormsModule, TranslateModule, FontAwesomeModule], + templateUrl: './browse.component.html', + styleUrl: './browse.component.css', +}) +export class BrowseComponent implements OnInit { + categories: Category[] = []; + searchQuery = ''; + loading = true; + faArrowRight = faArrowRight; + faChevronLeft = faChevronLeft; + faChevronRight = faChevronRight; + iconForCategory = iconForCategory; + + private readonly POPULAR_PER_PAGE = 3; + private readonly POPULAR_TOTAL = 9; + private popularCandidates: PopularOffer[] = []; + popularLoading = true; + popularPage = 0; + popularDirection: 'init' | 'next' | 'prev' = 'init'; + + get popularOffers(): PopularOffer[] { + return this.popularCandidates.slice(0, this.POPULAR_TOTAL); + } + + + get popularPages(): PopularOffer[][] { + const pages: PopularOffer[][] = []; + for (let i = 0; i < this.popularOffers.length; i += this.POPULAR_PER_PAGE) { + pages.push(this.popularOffers.slice(i, i + this.POPULAR_PER_PAGE)); + } + return pages; + } + + get popularPageCount(): number { + return this.popularPages.length; + } + + constructor( + private api: ApiServiceService, + private router: Router, + private localStorage: LocalStorageService, + ) {} + + ngOnInit(): void { + this.loadCategories(); + this.loadPopularOffers(); + } + + private async loadCategories(): Promise { + try { + const roots = await this.api.getDefaultCategories(); + const domeRoot = (Array.isArray(roots) ? roots : []).find( + (c: any) => c?.name === 'DOME Categories', + ); + if (domeRoot?.id) { + const children = await this.api.getCategoriesByParentId(domeRoot.id); + this.categories = Array.isArray(children) ? children : []; + } else { + this.categories = []; + } + } catch (e) { + console.error('Error loading categories:', e); + } finally { + this.loading = false; + } + } + + private async loadPopularOffers(): Promise { + try { + const offsets = [0, 6, 12, 18, 24, 30, 36, 42]; + const batches = await Promise.all( + offsets.map(o => this.api.getProducts(o, undefined).catch(() => [])), + ); + + const seen = new Set(); + const pool: any[] = []; + for (const batch of batches) { + if (!Array.isArray(batch)) continue; + for (const o of batch) { + const id = o?.id; + if (id && !seen.has(id)) { + seen.add(id); + pool.push(o); + } + } + } + + const detailed = await this.api.getProductsDetails(pool); + + const candidates: PopularOffer[] = detailed + .map(offer => { + const attachments = (offer as any)?.attachment ?? []; + const profile = attachments.find((a: any) => a?.name === 'Profile Picture'); + const picture = profile ?? attachments.find((a: any) => { + const type = (a?.attachmentType ?? '').toLowerCase(); + return type === 'picture' || type.startsWith('image/'); + }); + return { + id: (offer as any).id, + name: (offer as any).name ?? '', + description: (offer as any).description ?? '', + imageUrl: picture?.url ?? '', + }; + }) + .filter(o => !!o.id && !!o.imageUrl); + + const validated = await Promise.all( + candidates.map(async o => ((await this.imageLoads(o.imageUrl)) ? o : null)), + ); + const valid = validated.filter((o): o is PopularOffer => o !== null); + for (let i = valid.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [valid[i], valid[j]] = [valid[j], valid[i]]; + } + this.popularCandidates = valid; + } catch (e) { + console.error('Error loading popular offers:', e); + this.popularCandidates = []; + } finally { + this.popularLoading = false; + } + } + + private imageLoads(url: string): Promise { + return new Promise(resolve => { + const img = new Image(); + img.onload = () => resolve(img.naturalWidth > 0 && img.naturalHeight > 0); + img.onerror = () => resolve(false); + img.src = url; + }); + } + + nextPopularPage(): void { + if (this.popularPageCount === 0) return; + this.popularDirection = 'next'; + this.popularPage = (this.popularPage + 1) % this.popularPageCount; + } + + prevPopularPage(): void { + if (this.popularPageCount === 0) return; + this.popularDirection = 'prev'; + this.popularPage = (this.popularPage - 1 + this.popularPageCount) % this.popularPageCount; + } + + setPopularPage(index: number): void { + if (index >= 0 && index < this.popularPageCount && index !== this.popularPage) { + this.popularDirection = index > this.popularPage ? 'next' : 'prev'; + this.popularPage = index; + } + } + + goToOffer(offer: PopularOffer): void { + this.router.navigate(['/search', offer.id]); + } + + onSearch(event: Event) { + event.preventDefault(); + if (this.searchQuery.trim()) { + this.router.navigate(['/search', { keywords: this.searchQuery.trim() }]); + } else { + this.router.navigate(['/search']); + } + } + + onCategoryClick(category: Category) { + localStorage.removeItem('selected_categories'); + this.localStorage.addCategoryFilter(category); + this.router.navigate(['/search']); + } + + onShowAll() { + localStorage.removeItem('selected_categories'); + this.router.navigate(['/search']); + } +} diff --git a/src/app/pages/product-details/product-details.component.html b/src/app/pages/product-details/product-details.component.html index 9f350da9..815495ca 100644 --- a/src/app/pages/product-details/product-details.component.html +++ b/src/app/pages/product-details/product-details.component.html @@ -1,689 +1,549 @@ -
- -
- -
- +
+
+ + + +
+
+

{{productOff?.name}}

- -
-
- Product image +
+
+ +
+ @if (showDescriptionReadMore) { + + }
-
-
- -
{{productOff?.name}}
- -
-
- -
-
- @if(orgInfo!=undefined){ -
- {{ 'CARD._owner' | translate }}: {{orgInfo.tradingName}} -
- } -
V: {{productOff?.version || 'latest'}}
- -
-
- @for (cat of categories; track cat.id) { - - } - - @if(complianceLevel=='NL'){ - - - No level - - } @else if(complianceLevel=='BL'){ - Baseline logo - } @else if(complianceLevel=='P') { - Professional logo - } @else { - Professional plus logo + {{contact.characteristic?.emailAddress}} +
} -
-
- + + +
+ Product image +
- -
-
-
    -
  • - -
  • - @if(prodSpec.productSpecCharacteristic != undefined && prodSpec.productSpecCharacteristic.length>0) { -
  • - -
  • - } - @if(attatchments.length>0){ -
  • - -
  • - } - @if(licenseTerm || productOff?.serviceLevelAgreement){ -
  • - -
  • - } - @if(prodSpec.productSpecificationRelationship != undefined && prodSpec.productSpecificationRelationship.length>0){ -
  • - -
  • - } -
- @if(check_logged){ - @if (!isCustom()) { - - } @else if(quotesEnabled) { - - } + +
+
+
+ + + + +
+ @if(check_logged){ + @if (!isCustom()) { + + } @else if(quotesEnabled) { + + } + } +
+ @if(check_logged) { + @if (!isCustom()) { + + } @else if(quotesEnabled) { + + } + } +
+ +
+ + @if (activeTab === 'overview') { +
+ +
+
+
+
+ + + +
+
+ Offer version + v{{productOff?.version || '1.0'}} +
+
+
+
+ + + +
+
+ Last update + {{productOff?.lastUpdate | date:'dd/MM/yy, HH:mm'}} +
+
+
+
+ + + +
+
+ Product name + {{prodSpec.name}} +
+
+ @if(prodSpec.productNumber){ +
+
+ + + +
+
+ Identification number + {{prodSpec.productNumber}} +
+
+ } + @if(prodSpec.brand){ +
+
+ + + +
+
+ Brand + {{prodSpec.brand}} +
+
+ } + @if(orgInfo!=undefined){ +
+
+ + + +
+ +
} +
- @if(check_logged) { - @if (!isCustom()) { - - } @else if(quotesEnabled) { - + + @if(prodSpec.description){ +
+

How does it work?

+ +
+ } + + @if(serviceSpecs.length > 0){ +
+

{{ 'PRODUCT_DETAILS._service_spec' | translate }}

+ @for(service of serviceSpecs; track service.id){ +
+ {{service.name}}: + +
} +
} -
-
+ @if(resourceSpecs.length > 0){ +
+

{{ 'PRODUCT_DETAILS._resource_spec' | translate }}

+ @for(resource of resourceSpecs; track resource.id){ +
+ {{resource.name}}: + +
+ } +
+ } -
-
-
-

Description

- - @if(serviceSpecs.length > 0){ -
-
{{ 'PRODUCT_DETAILS._service_spec' | translate }}:
- @for(service of serviceSpecs; track service.id){ -

{{service.name}}:

- } -
+ @if(prodSpec.productSpecificationRelationship != undefined && prodSpec.productSpecificationRelationship.length>0){ +

{{ 'PRODUCT_DETAILS._product_rels' | translate }}

+
+ @for (rel of prodSpec.productSpecificationRelationship; track rel.id) { +
+

{{rel.name}}

+
+

{{rel.relationshipType}}

+ @if(rel.relationshipType == 'dependency'){ + + } @else if(rel.relationshipType == 'migration'){ + + } @else if(rel.relationshipType == 'exclusivity'){ + + } @else if(rel.relationshipType == 'substitution'){ + + } +
+
+ } +
+ } + +
+ } + + @if (activeTab === 'features') { + @if(prodSpec.productSpecCharacteristic != undefined && prodChars.length>0) { +
+

{{ 'PRODUCT_DETAILS._product_chars' | translate }}

+
+ @for (char of prodChars; track char.id; let idx = $index) { +
+

{{char.name}}

+ @if (char.description) { + } - @if(resourceSpecs.length > 0){ -
-
{{ 'PRODUCT_DETAILS._resource_spec' | translate }}:
- @for(resource of resourceSpecs; track resource.id){ -

{{resource.name}}:

- } -
+ @if (isOptionalCharacteristic(char)) { + Optional } - -

{{ 'CARD._comp_profile' | translate }}

- - -
- @if(complianceLevel=='NL'){ -

- No level -

- }@else if(complianceLevel=='BL'){ - - } @else if(complianceLevel=='P') { - - } @else { - - } -

- {{ complianceDescription }} -

- @if (selfAtt) { - - {{ 'PRODUCT_DETAILS._self_attestation' | translate }} - } - @if(complianceProf.length>0){ -

- 3rd party certifications: -

-
- @for (char of complianceProf; track char.id) { -
-
- - -

{{char.name}}

- -
-
- }
+
+ } @empty { +
+ {{ 'PRODUCT_DETAILS._no_chars' | translate }} +
+ } +
+
+ } @else { +
+ No features available for this product. +
+ } + } + + @if (activeTab === 'pricing') { +

{{ 'PRODUCT_DETAILS._product_pricing' | translate }}

+ @if(productOff?.productOfferingPrice != undefined){ + @if(checkCustom){ +
+ @for (price of productOff?.productOfferingPrice; track price.id) { + @if (price.priceType == 'custom') { +
+

{{price.name}}

+ +
} - @if(additionalCerts.length>0){ -

- Additional certifications: -

-
- @for (char of additionalCerts; track char.id) { -
-
- -

{{normalizeName(char.name)}}

- -
-
- } -
- } + } + @if(productOff?.productOfferingPrice?.length==0){ +
+

{{ 'SHOPPING_CART._free' | translate }}

+

{{ 'SHOPPING_CART._free_desc' | translate }}

+
+ } +
+ } @else { +
+ @for (price of productOff?.productOfferingPrice; track price.id) { +
+
+

{{price.name}}

+
+
+ +
+
+ } + @if(productOff?.productOfferingPrice?.length==0){ +
+
+

{{ 'SHOPPING_CART._free' | translate }}

+
+

{{ 'SHOPPING_CART._free_desc' | translate }}

+
+ }
+ } + } -

{{ 'PRODUCT_DETAILS._product_pricing' | translate }}

- @if(productOff?.productOfferingPrice != undefined){ - @if(checkCustom){ -
- @for (price of productOff?.productOfferingPrice; track price.id) { - @if (price.priceType == 'custom') { -
-
-

{{price.name}}

-
- -
- } - } - @if(productOff?.productOfferingPrice?.length==0){ -
-
-
-
{{ 'SHOPPING_CART._free' | translate }}
-
-

{{ 'SHOPPING_CART._free_desc' | translate }}

-
-
- } -
+ @if(usageMetrics.length > 0){ +

Usage metrics

+
+ @for (metric of usageMetrics; track metric.id) { +
+

{{metric.name}}

+ +
+ } +
+ } + } + + @if (activeTab === 'compliance') { +
+

Certification

+
+
+
+ + + +
+
+ @if(complianceLevel=='NL'){ +

No level

+ }@else if(complianceLevel=='BL'){ +

Baseline

+ } @else if(complianceLevel=='P') { +

Professional

} @else { -
- @for (price of productOff?.productOfferingPrice; track price.id) { - @if (price.priceType == 'recurring') { -
-
-
{{price.name}}
-
-
- -
-
- } @else if (price.priceType == 'usage') { -
-
-
{{price.name}}
-
-
- -
-
- } @else { -
-
-
{{price.name}}
-
-
- -
-
- } - } - @if(productOff?.productOfferingPrice?.length==0){ -
-
-
{{ 'SHOPPING_CART._free' | translate }}
-
-

{{ 'SHOPPING_CART._free_desc' | translate }}

-
- } -
+

Professional+

} +

{{ complianceDescription }}

+
+
+ @if (selfAtt) { + + {{ 'PRODUCT_DETAILS._self_attestation' | translate }} + } +
+
- @if(usageMetrics.length > 0){ -

Usage metrics

-
-
- @for (metric of usageMetrics; track metric.id) { -
-
-

{{metric.name}}

- -
-
- } -
+ @if(complianceProf.length>0){ +
+

Key Capabilities

+
+ @for (char of complianceProf; track char.id) { +
+
+
+

{{char.name}}

+
} - +
- -
- @if(prodSpec.productSpecCharacteristic != undefined && prodChars.length>0) { -
-

{{ 'PRODUCT_DETAILS._product_chars' | translate }}

-
-
- @for (char of prodChars; track char.id; let idx = $index) { -
-
-

{{char.name}}

- @if (char.description) { - - } - @if (isOptionalCharacteristic(char)) { - Optional - } -
- @if (isBooleanCharacteristic(char)) { -
- Default - - {{ getBooleanDefaultValue(char) ? 'Enabled' : 'Disabled' }} - -
- } @else { - @for (val of char?.productSpecCharacteristicValue; track val.value) { - @if (val?.isDefault == true) { -
- - @if(val.value !== undefined && val.value !== null){ - - } @else { - - } -
- } @else { -
- - @if(val.value !== undefined && val.value !== null){ - - } @else { - - } -
- } - } - } -
-
-
- } @empty { -
- -
- } -
-
-
- } -
- @if(attatchments.length>0){ -

{{ 'PRODUCT_DETAILS._product_att' | translate }}

-
-
- @for (att of attatchments; track att.id) { -
-
-
-

{{att.name}}

-
- -
- -
-
-
- } @empty { -
- -
- } -
+ @if(additionalCerts.length>0){ +
+

Additional certifications

+
+ @for (char of additionalCerts; track char.id) { +
+
+ +
+

{{normalizeName(char.name)}}

+
+ } +
- } -
-
+ @if(licenseTerm || productOff?.serviceLevelAgreement){ - - @if(licenseTerm && licenseTerm?.description != ''){ -

{{ 'PRODUCT_DETAILS._license' | translate }}

-
-
-
- -
- -
-
- -
- - -
- - @if(showReadMoreButton){ - - } - -
-
- } - @if(productOff?.serviceLevelAgreement){ -

{{ 'PRODUCT_DETAILS._sla' | translate }}

-

{{productOff?.serviceLevelAgreement?.name}}.

- } + @if(licenseTerm && licenseTerm?.description != ''){ +

{{ 'PRODUCT_DETAILS._license' | translate }}

+
+
+ +
+
+ +
+ @if(showReadMoreButton){ + + } +
+ } + @if(productOff?.serviceLevelAgreement){ +

{{ 'PRODUCT_DETAILS._sla' | translate }}

+

{{productOff?.serviceLevelAgreement?.name}}.

+ } } -
-
- @if(prodSpec.productSpecificationRelationship != undefined && prodSpec.productSpecificationRelationship.length>0){ -

{{ 'PRODUCT_DETAILS._product_rels' | translate }}

-
-
- @for (rel of prodSpec.productSpecificationRelationship; track rel.id) { -
-
-

{{rel.name}}

-
-

{{rel.relationshipType}}

- @if(rel.relationshipType == 'dependency'){ - - } @else if(rel.relationshipType == 'migration'){ - - } @else if(rel.relationshipType == 'exclusivity'){ - - } @else if(rel.relationshipType == 'substitution'){ - - } - -
-
-
- } -
-
+ @if(attatchments.length>0){ +

{{ 'PRODUCT_DETAILS._product_att' | translate }}

+
+ @for (att of attatchments; track att.id) { +
+

{{att.name}}

+ +
+ } +
} + } + + @if (!check_logged) { +
+

Ready to get started?

+

Join thousands of companies using {{productOff?.name}}

+ + Join DOME Marketplace + +
+ }
+
@if (toastVisibility) { -