diff --git a/.gitignore b/.gitignore index 313ae29..b92e632 100644 --- a/.gitignore +++ b/.gitignore @@ -35,6 +35,9 @@ npm-debug.log testem.log /typings +# Secrets +/cms/.env + # e2e /build diff --git a/bcmi/src/app/app.module.ts b/bcmi/src/app/app.module.ts index 33f02ce..556013a 100644 --- a/bcmi/src/app/app.module.ts +++ b/bcmi/src/app/app.module.ts @@ -38,6 +38,7 @@ import { LeafletModule } from '@bluehalo/ngx-leaflet'; import { GraphQLModule } from './graphql.module'; import { PageComponent } from './static-pages/page/page.component'; import { ContentService } from './services/content-service'; +import { ContentDirective } from './services/content-directive'; export function initConfig(configService: ConfigService) { return () => configService.init(); @@ -57,7 +58,8 @@ export function initConfig(configService: ConfigService) { WaterQualityComponent, TailingsManagementComponent, ReclamationComponent, - NotFoundComponent + NotFoundComponent, + ContentDirective ], imports: [ TagInputModule, diff --git a/bcmi/src/app/app.routes.ts b/bcmi/src/app/app.routes.ts index 7c56bf2..9f1e661 100644 --- a/bcmi/src/app/app.routes.ts +++ b/bcmi/src/app/app.routes.ts @@ -1,65 +1,14 @@ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; -import { AuthorizationsComponent } from './static-pages/authorizations/authorizations.component'; -//import { ComplianceOversightComponent } from './static-pages/compliance-oversight/compliance-oversight.component'; -import { ContactComponent } from './static-pages/contact/contact.component'; import { HomeComponent } from './home/home.component'; -//import { LegislationComponent } from './static-pages/legislation/legislation.component'; -//import { LifecycleComponent } from './static-pages/lifecycle/lifecycle.component'; -import { ReclamationComponent } from './static-pages/reclamation/reclamation.component'; -import { TailingsManagementComponent } from './static-pages/tailings-management/tailings-management.component'; -import { TopicsOfInterestComponent } from './static-pages/topics-of-interest/topics-of-interest.component'; -import { WaterQualityComponent } from './static-pages/water-quality/water-quality.component'; import { MainMapComponent } from './map/main-map/main-map.component'; import { NotFoundComponent } from './not-found/not-found.component'; export const routes: Routes = [ - { - path: 'authorizations', - component: AuthorizationsComponent - }, - /* Commenting out for demo - { - path: 'compliance-oversight', - component: ComplianceOversightComponent - }, - */ { path: '', component: HomeComponent }, - /* Commenting out for demo - { - path: 'legislation', - component: LegislationComponent - }, - */ - /* Commenting out for demo - { - path: 'lifecycle', - component: LifecycleComponent - }, - */ - { - path: 'reclamation', - component: ReclamationComponent - }, - { - path: 'tailings-management', - component: TailingsManagementComponent - }, - { - path: 'topics-of-interest', - component: TopicsOfInterestComponent - }, - { - path: 'water-quality', - component: WaterQualityComponent - }, - { - path: 'contact', - component: ContactComponent, - }, { path: 'map', component: MainMapComponent diff --git a/bcmi/src/app/models/content/header_button.ts b/bcmi/src/app/models/content/header_button.ts new file mode 100644 index 0000000..1d88a19 --- /dev/null +++ b/bcmi/src/app/models/content/header_button.ts @@ -0,0 +1,4 @@ +export class Header_Button { + Text: string; + Section_id: string; +} \ No newline at end of file diff --git a/bcmi/src/app/models/content/page.ts b/bcmi/src/app/models/content/page.ts index ca97c12..f455cda 100644 --- a/bcmi/src/app/models/content/page.ts +++ b/bcmi/src/app/models/content/page.ts @@ -1,10 +1,16 @@ +import { Header_Button } from "./header_button" + +// When adding new properties to this class, edit the query in /src/app/services/content-resolver.ts to include the new property export class Page { Title: string Description: string + Header_button: Header_Button[] // strapi doesn't allow renaming :( I wish I could add an s Content: string + // If you're adding a new sidecard variable, please add it to the isTwoColumn conditional as well! Ongoing_card: string External_card: string Related_card: string + Enforcement_Actions_card: string route: string tooltip: string } diff --git a/bcmi/src/app/services/content-directive.ts b/bcmi/src/app/services/content-directive.ts new file mode 100644 index 0000000..6454bb8 --- /dev/null +++ b/bcmi/src/app/services/content-directive.ts @@ -0,0 +1,48 @@ +import { Directive, ElementRef, Input, Renderer2, ViewContainerRef, AfterViewInit } from "@angular/core"; +import { DynamicLinkComponent } from "@app/shared/dynamic-link"; + +@Directive({ + selector: "[appContent]", +}) + +export class ContentDirective implements AfterViewInit { + @Input() + appStyle = true; + constructor( + private ref: ElementRef, + private viewContainerRef: ViewContainerRef, + private renderer: Renderer2, + ) {} + + ngAfterViewInit() { + const elements = Array.from(this.ref.nativeElement.getElementsByTagName("a")); + if(elements.length){ + elements.forEach( (link: HTMLAnchorElement) => { + if(link.hasAttribute("href") && link.getAttribute("href")[0] == '/'){ + this.replaceLink(link,link.getAttribute("href")); + } + }) + } + } + private replaceLink(anchor: HTMLAnchorElement, href: string) { + let processedHref = href; + // Create a new component dynamically + const componentRef = this.viewContainerRef.createComponent(DynamicLinkComponent); + + // Handle fragment if the link has one + const hashtagIndex = href.indexOf('#'); + if (hashtagIndex != -1) { + processedHref = href.substring(0, hashtagIndex); + componentRef.instance.fragment = href.substring(hashtagIndex + 1); + } + + componentRef.instance.routerLink = processedHref; + componentRef.instance.linkHTML = anchor.innerHTML; + componentRef.instance.linkClass = anchor.className; + + // Append the newly created component and destroy the old one + this.renderer.insertBefore(anchor.parentElement, componentRef.location.nativeElement,anchor); + this.renderer.removeChild(this.ref.nativeElement, anchor); + } + +} \ No newline at end of file diff --git a/bcmi/src/app/services/content-resolver.ts b/bcmi/src/app/services/content-resolver.ts index 602b704..25ba7f3 100644 --- a/bcmi/src/app/services/content-resolver.ts +++ b/bcmi/src/app/services/content-resolver.ts @@ -13,6 +13,7 @@ export class ContentResolver implements Resolve { private getPage = function(route){ + // When adding new properties to the Page class, edit this query return gql` { pageByRoute(route: "${route}") { @@ -20,10 +21,15 @@ export class ContentResolver implements Resolve { attributes{ Title, Description + Header_button{ + Text + Section_id + } Content Ongoing_card External_card Related_card + Enforcement_Actions_card route tooltip } diff --git a/bcmi/src/app/shared/dynamic-link.ts b/bcmi/src/app/shared/dynamic-link.ts new file mode 100644 index 0000000..bdb5a11 --- /dev/null +++ b/bcmi/src/app/shared/dynamic-link.ts @@ -0,0 +1,15 @@ +import { Component, Input } from '@angular/core'; +import { Router } from '@angular/router'; + +@Component({ + selector: 'app-dynamic-link', + template: ``, +}) +export class DynamicLinkComponent { + @Input() routerLink: string; + @Input() fragment:string | undefined; + @Input() linkHTML: string; + @Input() linkClass: string; + + constructor(private router: Router) {} +} diff --git a/bcmi/src/app/shared/shared.module.ts b/bcmi/src/app/shared/shared.module.ts index 92c14e9..684813b 100644 --- a/bcmi/src/app/shared/shared.module.ts +++ b/bcmi/src/app/shared/shared.module.ts @@ -7,6 +7,7 @@ import { ProjectStatusFilterPipe } from '@pipes/project-status-filter.pipe'; import { RemoveStringValuePipe } from '@pipes/remove-string-value.pipe'; import { SafeHtmlPipe } from '@pipes/safe-html.pipe'; import { RouterModule } from '@angular/router'; +import { DynamicLinkComponent } from './dynamic-link'; @NgModule({ imports: [ @@ -19,7 +20,8 @@ import { RouterModule } from '@angular/router'; ProjectTypeFilterPipe, ProjectStatusFilterPipe, RemoveStringValuePipe, - SafeHtmlPipe + SafeHtmlPipe, + DynamicLinkComponent ], exports: [ ObjectFilterPipe, diff --git a/bcmi/src/app/static-pages/page/page.component.html b/bcmi/src/app/static-pages/page/page.component.html index d62b1d4..79aec7c 100644 --- a/bcmi/src/app/static-pages/page/page.component.html +++ b/bcmi/src/app/static-pages/page/page.component.html @@ -4,6 +4,13 @@

{{pageData.Title}}

{{pageData.Description}}

+
@@ -11,32 +18,28 @@

{{pageData.Title}}

-
+
diff --git a/bcmi/src/app/static-pages/page/page.component.spec.ts b/bcmi/src/app/static-pages/page/page.component.spec.ts index f66f2c6..1c04f5e 100644 --- a/bcmi/src/app/static-pages/page/page.component.spec.ts +++ b/bcmi/src/app/static-pages/page/page.component.spec.ts @@ -1,4 +1,5 @@ import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; +import { SafeHtmlPipe } from '@pipes/safe-html.pipe'; import { PageComponent } from './page.component'; import { ActivatedRoute } from '@angular/router'; @@ -15,12 +16,20 @@ describe('PageComponent', () => { const pageData = new Page(); pageData.Title = "Test"; - const fakeActivatedRoute = {data: of({pageData: [pageData]}) }; + const fakeActivatedRoute = { + data: of({pageData: [pageData]}), + fragment: of(null), + snapshot: { + routeConfig: { + path: '', + }, + }, + }; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ providers: [{provide: ActivatedRoute, useValue: fakeActivatedRoute}], - declarations: [PageComponent], + declarations: [PageComponent, SafeHtmlPipe], imports: [CommonModule] }) .compileComponents(); diff --git a/bcmi/src/app/static-pages/page/page.component.ts b/bcmi/src/app/static-pages/page/page.component.ts index c004f3a..d528061 100644 --- a/bcmi/src/app/static-pages/page/page.component.ts +++ b/bcmi/src/app/static-pages/page/page.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit, ViewEncapsulation } from '@angular/core'; +import { afterRender, Component, OnInit, ViewEncapsulation } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { Page } from '../../models/content/page'; @@ -11,18 +11,42 @@ import { Page } from '../../models/content/page'; export class PageComponent implements OnInit { pageData: Page; + routerLink: string; + initialFragment: string | null; isTwoColumn: boolean; + handledFragmentFlag: boolean; - constructor(private activatedRoute: ActivatedRoute){}; + constructor(private activatedRoute: ActivatedRoute){ + this.initialFragment = null; + this.routerLink = "/" + activatedRoute.snapshot.routeConfig.path; + + afterRender(() => { + // Only need to do this once after our content loaded + if (this.handledFragmentFlag) { return; } + const contentHasLoaded = document.getElementById('page-content').hasChildNodes(); + if (contentHasLoaded && this.initialFragment) { + this.jumpToSection(this.initialFragment); + this.handledFragmentFlag = true; + } + }); + }; ngOnInit() { - //window.scrollTo(0, 0); + window.scrollTo(0, 0); this.activatedRoute.data.subscribe((response: any) => { this.pageData = response.pageData; - }) + }); + this.activatedRoute.fragment.subscribe(fragment => { + this.initialFragment = fragment; + }); // This conditional will need to be changed whenever sidecards are added to the template - this.isTwoColumn = !!this.pageData.External_card || !!this.pageData.Ongoing_card || !!this.pageData.Related_card; - + this.isTwoColumn = !!this.pageData.External_card || !!this.pageData.Ongoing_card || !!this.pageData.Related_card || !!this.pageData.Enforcement_Actions_card; + } + + jumpToSection(section: string | null) { + if (section) { + document.getElementById(section)?.scrollIntoView({ behavior: 'smooth' }); + } } } diff --git a/cms/src/api/page/content-types/page/schema.json b/cms/src/api/page/content-types/page/schema.json index 3421171..bb0b82d 100644 --- a/cms/src/api/page/content-types/page/schema.json +++ b/cms/src/api/page/content-types/page/schema.json @@ -57,12 +57,17 @@ "tooltip": { "type": "text" }, - "Enforcement_card": { + "Enforcement_Actions_card": { "type": "customField", "options": { "preset": "toolbar" }, "customField": "plugin::ckeditor5.CKEditor" + }, + "Header_button": { + "type": "component", + "repeatable": true, + "component": "page.scroll-button" } } } diff --git a/cms/src/components/page/scroll-button.json b/cms/src/components/page/scroll-button.json new file mode 100644 index 0000000..f56d347 --- /dev/null +++ b/cms/src/components/page/scroll-button.json @@ -0,0 +1,16 @@ +{ + "collectionName": "components_page_scroll_buttons", + "info": { + "displayName": "Scroll Button", + "description": "" + }, + "options": {}, + "attributes": { + "Text": { + "type": "string" + }, + "Section_id": { + "type": "string" + } + } +} diff --git a/cms/types/generated/components.d.ts b/cms/types/generated/components.d.ts index 649fe4d..70179d4 100644 --- a/cms/types/generated/components.d.ts +++ b/cms/types/generated/components.d.ts @@ -1,5 +1,17 @@ import type { Schema, Attribute } from '@strapi/strapi'; +export interface PageScrollButton extends Schema.Component { + collectionName: 'components_page_scroll_buttons'; + info: { + displayName: 'Scroll Button'; + description: ''; + }; + attributes: { + Text: Attribute.String; + Section_id: Attribute.String; + }; +} + export interface PageFeatureBlock extends Schema.Component { collectionName: 'components_page_feature_blocks'; info: { @@ -21,6 +33,7 @@ export interface PageFeatureBlock extends Schema.Component { declare module '@strapi/types' { export module Shared { export interface Components { + 'page.scroll-button': PageScrollButton; 'page.feature-block': PageFeatureBlock; } } diff --git a/cms/types/generated/contentTypes.d.ts b/cms/types/generated/contentTypes.d.ts index a2b5482..83e2f85 100644 --- a/cms/types/generated/contentTypes.d.ts +++ b/cms/types/generated/contentTypes.d.ts @@ -924,13 +924,14 @@ export interface ApiPagePage extends Schema.CollectionType { >; route: Attribute.String & Attribute.Required; tooltip: Attribute.Text; - Enforcement_card: Attribute.RichText & + Enforcement_Actions_card: Attribute.RichText & Attribute.CustomField< 'plugin::ckeditor5.CKEditor', { preset: 'toolbar'; } >; + Header_button: Attribute.Component<'page.scroll-button', true>; createdAt: Attribute.DateTime; updatedAt: Attribute.DateTime; publishedAt: Attribute.DateTime;