From 274cee8f7bfc02e2aa85d42b82b0ace15caf4044 Mon Sep 17 00:00:00 2001 From: Francisco de la Vega Date: Tue, 28 Jul 2026 19:35:05 +0200 Subject: [PATCH] Add support for news and FAQ as blog types --- cypress/e2e/blog.cy.ts | 2 +- cypress/support/constants.ts | 1 + src/app/app-routing.module.ts | 21 ++- src/app/app.module.ts | 4 +- src/app/offerings/faq/faq.component.css | 98 ++++++++++++ src/app/offerings/faq/faq.component.html | 112 ++++++++------ src/app/offerings/faq/faq.component.spec.ts | 25 +++- src/app/offerings/faq/faq.component.ts | 130 +++++++++++++++- .../blog-entry-detail.component.html | 4 +- .../blog-entry-detail.component.spec.ts | 2 +- .../blog-entry-detail.component.ts | 26 +++- .../pages/dome-blog/dome-blog.component.html | 20 ++- .../pages/dome-blog/dome-blog.component.ts | 105 ++++++++++++- .../entry-form/entry-form.component.html | 140 ++++++++++-------- .../entry-form/entry-form.component.spec.ts | 5 + .../entry-form/entry-form.component.ts | 138 ++++++++++++++--- .../dome-blog-service.service.spec.ts | 17 +++ src/app/services/dome-blog-service.service.ts | 23 ++- src/app/themes/dome.theme.ts | 5 + src/assets/i18n/en.json | 1 + src/assets/i18n/es.json | 1 + 21 files changed, 719 insertions(+), 161 deletions(-) diff --git a/cypress/e2e/blog.cy.ts b/cypress/e2e/blog.cy.ts index 53770ae9..8694b26c 100644 --- a/cypress/e2e/blog.cy.ts +++ b/cypress/e2e/blog.cy.ts @@ -12,7 +12,7 @@ describe('/blog',{ }) it('should create a blog entry correctly', () => { - cy.intercept({method: 'GET', url: '**/domeblog'}, {statusCode: 200, body: []}).as('blogGET') + cy.intercept({method: 'GET', url: '**/domeblog*'}, {statusCode: 200, body: []}).as('blogGET') cy.intercept({method: 'POST', url: '**/domeblog'}, {statusCode: 201, body: blogEntry}).as('blogPOST') cy.visit('/blog') cy.wait('@blogGET') diff --git a/cypress/support/constants.ts b/cypress/support/constants.ts index c7509316..572ad92c 100644 --- a/cypress/support/constants.ts +++ b/cypress/support/constants.ts @@ -344,6 +344,7 @@ export const productOffering = { ] } export const blogEntry = { + contentType: 'blog', title: 'Entry title', slug: 'entry-title', featuredImage: '', diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index dcf09ce0..07a32ff3 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -5,6 +5,7 @@ import { AboutDomeComponent } from "src/app/pages/about-dome/about-dome.componen import { BlogEntryDetailComponent } from "src/app/pages/dome-blog/blog-entry-detail/blog-entry-detail.component"; import { DomeBlogComponent } from "src/app/pages/dome-blog/dome-blog.component"; import { EntryFormComponent } from "src/app/pages/dome-blog/entry-form/entry-form.component"; +import { FaqComponent } from "src/app/offerings/faq/faq.component"; import { UsageSpecsComponent } from "src/app/pages/usage-specs/usage-specs.component"; import { AuthGuard } from './guard/auth.guard'; import { quoteGuardGuard } from './guard/quote-guard.guard'; @@ -126,11 +127,27 @@ const routes: Routes = [ }, { path: 'blog', - component: DomeBlogComponent + component: DomeBlogComponent, + data: { contentType: 'blog' } }, { path: 'blog/:slugOrId', - component: BlogEntryDetailComponent + component: BlogEntryDetailComponent, + data: { contentType: 'blog' } + }, + { + path: 'news', + component: DomeBlogComponent, + data: { contentType: 'news' } + }, + { + path: 'news/:slugOrId', + component: BlogEntryDetailComponent, + data: { contentType: 'news' } + }, + { + path: 'faq', + component: FaqComponent }, { path: 'blog-entry', diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 952108db..2257a51d 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -100,6 +100,7 @@ import { OperatorRevenueSharingComponent } from "src/app/pages/admin/operator-re import { ProviderRevenueSharingComponent } from "src/app/pages/user-profile/profile-sections/provider-revenue-sharing/provider-revenue-sharing.component"; import { MarkdownTextareaComponent } from "src/app/shared/forms/markdown-textarea/markdown-textarea.component"; import { RequestValidationModalComponent } from './pages/seller-offerings/offerings/seller-product-spec/update-product-spec/request-validation-modal/request-validation-modal.component'; +import { ConfirmDialogComponent } from './shared/confirm-dialog/confirm-dialog.component'; @NgModule({ declarations: [ @@ -204,7 +205,8 @@ import { RequestValidationModalComponent } from './pages/seller-offerings/offeri AboutDomeComponent, MarkdownTextareaComponent, ProviderRevenueSharingComponent, - OperatorRevenueSharingComponent + OperatorRevenueSharingComponent, + ConfirmDialogComponent ], providers: [ AppInitService, diff --git a/src/app/offerings/faq/faq.component.css b/src/app/offerings/faq/faq.component.css index e69de29b..2423929f 100644 --- a/src/app/offerings/faq/faq.component.css +++ b/src/app/offerings/faq/faq.component.css @@ -0,0 +1,98 @@ +:host { + display: block; +} + +.faq-card { + background: #fff; + border: 1px solid #d8dade; + border-radius: 8px; + padding: 24px; + transition: border-color 0.25s ease, box-shadow 0.25s ease; +} + +.faq-card--open { + box-shadow: 0 10px 26px rgba(11, 21, 40, 0.08); +} + +.faq-question { + width: 100%; + display: flex; + align-items: center; + gap: 24px; + background: transparent; + border: none; + padding: 0; + cursor: pointer; + text-align: left; + font: inherit; + color: inherit; +} + +.faq-question__title { + flex: 1; + color: #0b1528; + font-weight: 700; + font-size: 20px; + line-height: 26px; + overflow-wrap: anywhere; +} + +.faq-icon { + width: 40px; + height: 40px; + border-radius: 6px; + display: flex; + align-items: center; + justify-content: center; + background: #dde6f6; + color: #2d58a7; + flex-shrink: 0; + transition: background-color 0.25s ease, color 0.25s ease; +} + +.faq-icon--open { + background: #2d58a7; + color: #fff; +} + +.faq-answer { + display: grid; + grid-template-rows: 0fr; + opacity: 0; + margin-top: 0; + transition: grid-template-rows 0.25s ease, opacity 0.2s ease, margin-top 0.25s ease; +} + +.faq-answer--open { + grid-template-rows: 1fr; + opacity: 1; + margin-top: 20px; +} + +.faq-answer__inner { + min-height: 0; + overflow: hidden; +} + +.faq-divider { + height: 1px; + width: 100%; + background: #d8dade; + margin-bottom: 20px; +} + +.faq-answer__content { + color: #4c5a6b; + font-size: 17px; + line-height: 27px; + overflow-wrap: anywhere; +} + +:host-context(.dark) .faq-card { + background: var(--secondary-100, #1f2937); +} + +:host-context(.dark) .faq-question__title, +:host-context(.dark) .faq-answer__content { + color: #fff; +} diff --git a/src/app/offerings/faq/faq.component.html b/src/app/offerings/faq/faq.component.html index e4925d6e..2c43cb2c 100644 --- a/src/app/offerings/faq/faq.component.html +++ b/src/app/offerings/faq/faq.component.html @@ -1,58 +1,76 @@ -
-
-

FAQs

-

Lorem ipsum dolor sit amet, consectetur adipiscing elit, - sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris

- - - Go to FAQs - +
+
+
+
+

+ FAQ +

+ + +
+
+
+
+
+
+ - -
-
+
+ No FAQs have been published yet. +
+
+
+ + diff --git a/src/app/offerings/faq/faq.component.spec.ts b/src/app/offerings/faq/faq.component.spec.ts index efc33537..f1308f52 100644 --- a/src/app/offerings/faq/faq.component.spec.ts +++ b/src/app/offerings/faq/faq.component.spec.ts @@ -2,19 +2,29 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { NO_ERRORS_SCHEMA } from '@angular/core'; import { TranslateModule } from '@ngx-translate/core'; import { RouterTestingModule } from '@angular/router/testing'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { DomeBlogServiceService } from 'src/app/services/dome-blog-service.service'; +import { LocalStorageService } from 'src/app/services/local-storage.service'; import { FaqComponent } from './faq.component'; describe('FaqComponent', () => { let component: FaqComponent; let fixture: ComponentFixture; + let domeBlogService: jasmine.SpyObj; beforeEach(async () => { + domeBlogService = jasmine.createSpyObj('DomeBlogServiceService', ['getBlogEntries', 'deleteBlogEntry']); + domeBlogService.getBlogEntries.and.resolveTo([]); + domeBlogService.deleteBlogEntry.and.resolveTo({}); + await TestBed.configureTestingModule({ schemas: [NO_ERRORS_SCHEMA], - imports: [HttpClientTestingModule, RouterTestingModule, TranslateModule.forRoot()], - declarations: [FaqComponent] + imports: [RouterTestingModule, TranslateModule.forRoot()], + declarations: [FaqComponent], + providers: [ + { provide: DomeBlogServiceService, useValue: domeBlogService }, + { provide: LocalStorageService, useValue: { getObject: jasmine.createSpy('getObject').and.returnValue({}) } } + ] }) .compileComponents(); @@ -26,4 +36,13 @@ describe('FaqComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + it('should open one FAQ at a time', () => { + component.toggleFaq('one'); + expect(component.isOpen('one')).toBeTrue(); + + component.toggleFaq('two'); + expect(component.isOpen('one')).toBeFalse(); + expect(component.isOpen('two')).toBeTrue(); + }); }); diff --git a/src/app/offerings/faq/faq.component.ts b/src/app/offerings/faq/faq.component.ts index 06c68c49..400aa14d 100644 --- a/src/app/offerings/faq/faq.component.ts +++ b/src/app/offerings/faq/faq.component.ts @@ -1,10 +1,136 @@ -import { Component } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; +import moment from 'moment'; +import { LoginInfo } from 'src/app/models/interfaces'; +import { DomeBlogServiceService } from 'src/app/services/dome-blog-service.service'; +import { LocalStorageService } from 'src/app/services/local-storage.service'; @Component({ selector: 'app-faq', templateUrl: './faq.component.html', styleUrl: './faq.component.css' }) -export class FaqComponent { +export class FaqComponent implements OnInit { + faqs: any[] = []; + openFaqId = ''; + checkAdmin = false; + deletingEntryId: string | null = null; + showDeleteConfirm = false; + pendingDeleteEntry: any = null; + deleteConfirmTitle = 'Delete FAQ'; + deleteConfirmMessage = ''; + deleteConfirmButtonText = 'Delete'; + deleteConfirmButtonClass = 'px-4 py-2 text-sm font-medium text-white bg-red-700 border border-transparent rounded-md hover:bg-red-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500'; + + constructor( + private router: Router, + private domeBlogService: DomeBlogServiceService, + private localStorage: LocalStorageService + ) {} + + async ngOnInit(): Promise { + this.initPartyInfo(); + await this.loadFaqs(); + } + + async loadFaqs() { + try { + const response = await this.domeBlogService.getBlogEntries({ contentType: 'faq' }); + this.faqs = this.extractEntries(response).filter((entry) => this.isFaqEntry(entry)); + } catch (error) { + this.faqs = []; + } + } + + toggleFaq(id: string) { + this.openFaqId = this.openFaqId === id ? '' : id; + } + + isOpen(id: string): boolean { + return this.openFaqId === id; + } + + getEntryId(entry: any): string { + return entry?._id || entry?.id || entry?.slug || entry?.title; + } + + goToCreate() { + this.router.navigate(['/blog-entry'], { queryParams: { type: 'faq' } }); + } + + goToUpdate(entry: any) { + const id = entry?._id || entry?.id; + if (!id) { + return; + } + + this.router.navigate(['/blog-entry/', id], { queryParams: { type: 'faq' } }); + } + + canManageEntry(): boolean { + return this.checkAdmin; + } + + isDeletingEntry(entry: any): boolean { + return this.deletingEntryId === (entry?._id || entry?.id); + } + + openDeleteDialog(entry: any) { + const id = entry?._id || entry?.id; + if (!id || this.isDeletingEntry(entry)) { + return; + } + + this.pendingDeleteEntry = entry; + this.deleteConfirmMessage = `Are you sure you want to delete "${entry.title}"? This action cannot be undone.`; + this.showDeleteConfirm = true; + } + + closeDeleteDialog() { + this.showDeleteConfirm = false; + this.pendingDeleteEntry = null; + } + + async confirmDeleteEntry() { + const id = this.pendingDeleteEntry?._id || this.pendingDeleteEntry?.id; + if (!id) { + this.closeDeleteDialog(); + return; + } + + this.deletingEntryId = id; + this.closeDeleteDialog(); + try { + await this.domeBlogService.deleteBlogEntry(id); + await this.loadFaqs(); + } catch (error) { + console.error('There was an error while deleting the FAQ entry!', error); + } finally { + this.deletingEntryId = null; + } + } + + private initPartyInfo(){ + const aux = this.localStorage.getObject('login_items') as LoginInfo; + if(JSON.stringify(aux) != '{}' && (((aux.expire - moment().unix())-4) > 0)) { + this.checkAdmin=aux.roles.some(role => + role.name === 'admin' + ); + } + } + + private extractEntries(response: any): any[] { + if (Array.isArray(response)) { + return response; + } + + const possibleEntries = response?.items || response?.entries || response?.data || response?.results || response?.content; + return Array.isArray(possibleEntries) ? possibleEntries : []; + } + + private isFaqEntry(entry: any): boolean { + const contentType = (entry?.contentType || entry?.type || '').toString().trim().toLowerCase(); + return contentType === 'faq'; + } } diff --git a/src/app/pages/dome-blog/blog-entry-detail/blog-entry-detail.component.html b/src/app/pages/dome-blog/blog-entry-detail/blog-entry-detail.component.html index 4f1bdfe4..259c9640 100644 --- a/src/app/pages/dome-blog/blog-entry-detail/blog-entry-detail.component.html +++ b/src/app/pages/dome-blog/blog-entry-detail/blog-entry-detail.component.html @@ -1,5 +1,5 @@
-
+