From 299e49fe3c372743115242764c4f4107faf2c4f5 Mon Sep 17 00:00:00 2001 From: Scott Straughan Date: Wed, 25 Sep 2024 16:32:17 +0100 Subject: [PATCH 01/27] Created new safe-storage.service.ts that takes over the storing of all data to the persistent storage. The motivation behind this is to ensure that all cookies and data storage is defined and to ensure data is never stored if it isn't allowed. --- src/app/app.component.ts | 27 +-- src/app/pages/cookies/cookies.component.ts | 9 +- .../cookie-acceptance.component.ts | 16 +- .../video-view-popup.component.ts | 12 +- .../academy/lesson/lesson.component.ts | 9 +- .../pages/playground/playground.component.ts | 15 +- .../compiler-selector-popup.component.ts | 2 +- .../load-and-save-popup.component.ts | 32 ++-- .../platform-info-popup.component.scss | 2 +- src/app/pages/settings/settings.component.ts | 28 ++-- .../shared/services/safe-storage.service.ts | 156 ++++++++++++++++++ src/app/shared/services/state.service.ts | 120 -------------- src/environments/environment.ts | 10 +- 13 files changed, 244 insertions(+), 194 deletions(-) create mode 100644 src/app/shared/services/safe-storage.service.ts delete mode 100644 src/app/shared/services/state.service.ts diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 870120b..2a6d561 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -33,11 +33,11 @@ import { SiteWideSearchComponent } from './shared/components/site-wide-search/si import { environment } from '../environments/environment'; import { PopupService } from './shared/components/popup/popup.service'; import { SearchComponent } from './shared/components/search/search.component'; -import { StateService } from './shared/services/state.service'; import { map, Subscription } from 'rxjs'; import { CookieAcceptanceComponent } from './pages/cookies/shared/cookie-acceptance-popup/cookie-acceptance.component'; import { toSignal } from '@angular/core/rxjs-interop'; import { PlatformService } from './shared/services/platform.service'; +import { SafeStorageService } from './shared/services/safe-storage.service'; @Component({ selector: 'app-root', @@ -91,17 +91,22 @@ export class AppComponent implements OnDestroy { /** * Constructor. + * @param document + * @param renderer + * @param popupService + * @param safeStorageService + * @param platformService */ constructor( @Inject(DOCUMENT) protected document: Document, protected renderer: Renderer2, protected popupService: PopupService, - protected stateService: StateService, + protected safeStorageService: SafeStorageService, protected platformService: PlatformService, ) { if (this.platformService.isClient()) { - this.darkModeEnabled = toSignal(this.stateService.getObservable().pipe( - map((state) => state.darkModeEnabled), + this.darkModeEnabled = toSignal(this.safeStorageService.observe().pipe( + map((state) => state['st-dark-mode-enabled']), ), { initialValue: false }); effect(() => { @@ -112,10 +117,10 @@ export class AppComponent implements OnDestroy { } }); - this.state$ = this.stateService.getObservable().subscribe((state) => { + this.state$ = this.safeStorageService.observe().subscribe((state) => { const fathomTrackers = this.document.documentElement.getElementsByClassName('fathom-tracking-script'); - if (state.enableTracking && fathomTrackers.length === 0) { + if (state['st-enable-tracking'] && fathomTrackers.length === 0) { // Add fathom Analytics const fathom = document.createElement('script'); fathom.setAttribute('class', 'fathom-tracking-script'); @@ -126,16 +131,16 @@ export class AppComponent implements OnDestroy { // Add to dom this.document.head.appendChild(fathom); - } else if (!state.enableTracking && fathomTrackers.length > 0) { + } else if (!state['st-enable-tracking'] && fathomTrackers.length > 0) { fathomTrackers[0].remove(); } - this.enableDarkModeSwitch.set(state.cookiesAccepted ? state.cookiesAccepted : false); + this.enableDarkModeSwitch.set(this.safeStorageService.allowed()); }); } - this.darkModeEnabled = toSignal(this.stateService.getObservable().pipe( - map((state) => state.darkModeEnabled) + this.darkModeEnabled = toSignal(this.safeStorageService.observe().pipe( + map((state) => state['st-dark-mode-enabled']) ), { initialValue: false }); effect(() => { @@ -163,7 +168,7 @@ export class AppComponent implements OnDestroy { return ; } - this.stateService.setDarkMode(!this.darkModeEnabled()); + this.safeStorageService.save('st-dark-mode-enabled', !this.darkModeEnabled()); } /** diff --git a/src/app/pages/cookies/cookies.component.ts b/src/app/pages/cookies/cookies.component.ts index 42ceabd..a6fc631 100644 --- a/src/app/pages/cookies/cookies.component.ts +++ b/src/app/pages/cookies/cookies.component.ts @@ -19,8 +19,9 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'; import { CommonModule } from '@angular/common'; import { environment } from '../../../environments/environment'; -import { StateService } from '../../shared/services/state.service'; import { Title } from '@angular/platform-browser'; +import { SafeStorageService } from '../../shared/services/safe-storage.service'; +import { CookieAcceptanceComponent } from './shared/cookie-acceptance-popup/cookie-acceptance.component'; @Component({ selector: 'st-cookies', @@ -37,11 +38,11 @@ export class CookiesComponent { /** * Constructor. - * @param stateService + * @param safeStorageService * @param title */ constructor( - protected stateService: StateService, + protected safeStorageService: SafeStorageService, protected title: Title ) { this.title.setTitle('Cookies - SYCL.tech'); @@ -51,6 +52,6 @@ export class CookiesComponent { * Called when a user wishes to change their cookie/storage acceptance. */ onStorageAcceptance() { - this.stateService.setStoragePolicyAccepted(undefined); + this.safeStorageService.clear(SafeStorageService.STORAGE_ALLOWED_KEY); } } diff --git a/src/app/pages/cookies/shared/cookie-acceptance-popup/cookie-acceptance.component.ts b/src/app/pages/cookies/shared/cookie-acceptance-popup/cookie-acceptance.component.ts index ac2bfdc..69847c9 100644 --- a/src/app/pages/cookies/shared/cookie-acceptance-popup/cookie-acceptance.component.ts +++ b/src/app/pages/cookies/shared/cookie-acceptance-popup/cookie-acceptance.component.ts @@ -19,10 +19,10 @@ import { ChangeDetectionStrategy, Component, signal, Signal } from '@angular/core'; import { AsyncPipe } from '@angular/common'; import { RouterLink } from '@angular/router'; -import { StateService } from '../../../../shared/services/state.service'; import { toSignal } from '@angular/core/rxjs-interop'; import { map } from 'rxjs'; import { PlatformService } from '../../../../shared/services/platform.service'; +import { SafeStorageService, State } from '../../../../shared/services/safe-storage.service'; @Component({ selector: 'st-cookie-acceptance', @@ -40,18 +40,18 @@ export class CookieAcceptanceComponent { /** * Constructor. - * @param stateService + * @param safeStorageService * @param platformService */ constructor( - protected stateService: StateService, + protected safeStorageService: SafeStorageService, protected platformService: PlatformService ) { // Only show the dialog on client to avoid flickering (dialog will be rendered during pre-rendering). if (this.platformService.isClient()) { - this.show = toSignal(this.stateService.getObservable().pipe( - map((state) => { - return (state.cookiesAccepted === undefined); + this.show = toSignal(this.safeStorageService.observe().pipe( + map((state: State) => { + return state[SafeStorageService.STORAGE_ALLOWED_KEY] == undefined; }), ), { initialValue: false }); } @@ -61,13 +61,13 @@ export class CookieAcceptanceComponent { * Called when a user accepts our policies. */ onAcceptPolicies() { - this.stateService.setStoragePolicyAccepted(true); + this.safeStorageService.save(SafeStorageService.STORAGE_ALLOWED_KEY, true); } /** * Called when a user rejects our policies. */ onRejectPolicies() { - this.stateService.setStoragePolicyAccepted(false); + this.safeStorageService.save(SafeStorageService.STORAGE_ALLOWED_KEY, false); } } diff --git a/src/app/pages/ecosystem/videos/video-widget/video-view-popup/video-view-popup.component.ts b/src/app/pages/ecosystem/videos/video-widget/video-view-popup/video-view-popup.component.ts index fded5a6..daf119a 100644 --- a/src/app/pages/ecosystem/videos/video-widget/video-view-popup/video-view-popup.component.ts +++ b/src/app/pages/ecosystem/videos/video-widget/video-view-popup/video-view-popup.component.ts @@ -23,7 +23,6 @@ import { MarkdownComponent } from 'ngx-markdown'; import { RouterLink } from '@angular/router'; import { VideoModel } from '../../../../../shared/models/video.model'; import { PopupReference } from '../../../../../shared/components/popup/popup.service'; -import { StateService } from '../../../../../shared/services/state.service'; import { map } from 'rxjs'; import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; import { toSignal } from '@angular/core/rxjs-interop'; @@ -31,6 +30,7 @@ import { ContributorAvatarComponent } from '../../../../../shared/components/contributor-avatar/contributor-avatar.component'; import { MultiDateComponent } from '../../../../../shared/components/multi-date/multi-date.component'; +import { SafeStorageService } from '../../../../../shared/services/safe-storage.service'; @Component({ selector: 'st-video-view-popup', @@ -78,12 +78,12 @@ export class VideoViewPopupComponent { /** * Constructor. * @param popupReference - * @param stateService + * @param safeStorageService * @param sanitizer */ constructor( @Inject('POPUP_DATA') popupReference: PopupReference, - private stateService: StateService, + private safeStorageService: SafeStorageService, private sanitizer: DomSanitizer, ) { this.video = popupReference.data['video']; @@ -98,10 +98,8 @@ export class VideoViewPopupComponent { return embedUrl ? this.sanitizer.bypassSecurityTrustResourceUrl(embedUrl) : undefined; }); - this.cookiesEnabled = toSignal(this.stateService.getObservable().pipe( - map((state) => { - return !!state.cookiesAccepted; - }) + this.cookiesEnabled = toSignal(this.safeStorageService.observe().pipe( + map(() => this.safeStorageService.allowed()) )); } } diff --git a/src/app/pages/getting-started/academy/lesson/lesson.component.ts b/src/app/pages/getting-started/academy/lesson/lesson.component.ts index e53551c..be01668 100644 --- a/src/app/pages/getting-started/academy/lesson/lesson.component.ts +++ b/src/app/pages/getting-started/academy/lesson/lesson.component.ts @@ -28,10 +28,10 @@ import { AcademyLessonService } from '../../../../shared/services/models/academy import { MonacoEditorModule } from 'ngx-monaco-editor-v2'; import { FormsModule } from '@angular/forms'; import { PlatformService } from '../../../../shared/services/platform.service'; -import { StateService } from '../../../../shared/services/state.service'; import { toSignal } from '@angular/core/rxjs-interop'; import { TabComponent } from '../../../../shared/components/tabs/tab/tab.component'; import { TabsComponent } from '../../../../shared/components/tabs/tabs.component'; +import { SafeStorageService } from '../../../../shared/services/safe-storage.service'; @Component({ selector: 'st-lesson', @@ -69,7 +69,7 @@ export class LessonComponent { * @param activatedRoute * @param academyLessonService * @param platformService - * @param stateService + * @param safeStorageService * @param titleService * @param meta * @param location @@ -78,7 +78,7 @@ export class LessonComponent { protected activatedRoute: ActivatedRoute, protected academyLessonService: AcademyLessonService, protected platformService: PlatformService, - protected stateService: StateService, + protected safeStorageService: SafeStorageService, protected titleService: Title, protected meta: Meta, protected location: Location @@ -90,7 +90,8 @@ export class LessonComponent { this.lessons = toSignal(this.academyLessonService.all(), { initialValue: [] }); this.monacoEditorTheme = toSignal( - this.stateService.getObservable().pipe(map(state => state.darkModeEnabled ? 'st-dark' : 'vs-light')), + this.safeStorageService.observe().pipe( + map(state => state['st-dark-mode-enabled'] ? 'st-dark' : 'vs-light')), { initialValue: 'vs-light' }) if (this.platformService.isClient()) { diff --git a/src/app/pages/playground/playground.component.ts b/src/app/pages/playground/playground.component.ts index 707aafb..32eead4 100644 --- a/src/app/pages/playground/playground.component.ts +++ b/src/app/pages/playground/playground.component.ts @@ -42,15 +42,14 @@ import { Meta, Title } from '@angular/platform-browser'; import { PlaygroundSampleService } from '../../shared/services/models/playground-sample.service'; import { AlertBubbleComponent } from '../../shared/components/alert-bubble/alert-bubble.component'; import { SearchablePage } from '../../shared/components/site-wide-search/SearchablePage'; -import { StateService } from '../../shared/services/state.service'; import { toSignal } from '@angular/core/rxjs-interop'; import { ActivatedRoute, Params, Router } from '@angular/router'; -import { LOCAL_STORAGE, StorageService } from 'ngx-webstorage-service'; import { LoadAndSavePopupComponent } from './popups/load-and-save/load-and-save-popup.component'; import { SampleChooserComponent } from './popups/sample-chooser/sample-chooser.component'; import { PlatformInfoPopupComponent } from './popups/platform-info/platform-info-popup.component'; import { SharePopupComponent } from './popups/share/share-popup.component'; import { CompilerSelectorPopupComponent } from './popups/compiler-select/compiler-selector-popup.component'; +import { SafeStorageService } from '../../shared/services/safe-storage.service'; @Component({ selector: 'st-playground', @@ -100,9 +99,8 @@ export class PlaygroundComponent implements SearchablePage, OnInit, OnDestroy { * @param popupService * @param platformService * @param playgroundService - * @param stateService + * @param safeStorageService * @param activatedRoute - * @param storageService * @param document * @param renderer * @param router @@ -113,9 +111,8 @@ export class PlaygroundComponent implements SearchablePage, OnInit, OnDestroy { protected popupService: PopupService, protected platformService: PlatformService, protected playgroundService: PlaygroundService, - protected stateService: StateService, + protected safeStorageService: SafeStorageService, protected activatedRoute: ActivatedRoute, - @Inject(LOCAL_STORAGE) protected storageService: StorageService, @Inject(DOCUMENT) protected document: Document, protected renderer: Renderer2, protected router: Router @@ -174,7 +171,7 @@ export class PlaygroundComponent implements SearchablePage, OnInit, OnDestroy { if (sample == undefined) { this.setSample(PlaygroundSampleService.getDefaultSample()); - if (this.storageService.has(LoadAndSavePopupComponent.storageKey)) { + if (this.safeStorageService.has(LoadAndSavePopupComponent.storageKey)) { this.onSaveLoadSample(); } else if (this.platformService.isClient()) { this.onChooseSample(); @@ -210,8 +207,8 @@ export class PlaygroundComponent implements SearchablePage, OnInit, OnDestroy { * Get the editor theme. */ getEditorTheme(): Observable { - return this.stateService.getObservable().pipe( - map(state => state.darkModeEnabled ? 'st-dark' : 'vs-light')); + return this.safeStorageService.observe().pipe( + map(state => state['st-dark-mode-enabled'] ? 'st-dark' : 'vs-light')); } /** diff --git a/src/app/pages/playground/popups/compiler-select/compiler-selector-popup.component.ts b/src/app/pages/playground/popups/compiler-select/compiler-selector-popup.component.ts index d32e496..4068843 100644 --- a/src/app/pages/playground/popups/compiler-select/compiler-selector-popup.component.ts +++ b/src/app/pages/playground/popups/compiler-select/compiler-selector-popup.component.ts @@ -86,7 +86,7 @@ export class CompilerSelectorPopupComponent implements OnInit { * @param compiler */ onSelectCompiler(compiler: PlaygroundCompiler) { - this.playgroundService.selectedCompiler.set(compiler); + this.playgroundService.setCompiler(compiler); this.popupReference.close(compiler); } diff --git a/src/app/pages/playground/popups/load-and-save/load-and-save-popup.component.ts b/src/app/pages/playground/popups/load-and-save/load-and-save-popup.component.ts index a736613..a0ccfcc 100644 --- a/src/app/pages/playground/popups/load-and-save/load-and-save-popup.component.ts +++ b/src/app/pages/playground/popups/load-and-save/load-and-save-popup.component.ts @@ -19,11 +19,10 @@ import { ChangeDetectionStrategy, Component, Inject, OnInit, signal, WritableSignal } from '@angular/core'; import { LoadingComponent } from '../../../../shared/components/loading/loading.component'; import { DatePipe } from '@angular/common'; -import { LOCAL_STORAGE, StorageService } from 'ngx-webstorage-service'; -import { StateService } from '../../../../shared/services/state.service'; import { tap } from 'rxjs'; import { PopupReference } from '../../../../shared/components/popup/popup.service'; import { RouterLink } from '@angular/router'; +import { SafeStorageService } from '../../../../shared/services/safe-storage.service'; @Component({ selector: 'st-load-popup', @@ -48,25 +47,23 @@ export class LoadAndSavePopupComponent implements OnInit { /** * Constructor. * @param popupReference - * @param storageService - * @param stateService + * @param safeStorageService */ constructor( @Inject('POPUP_DATA') protected popupReference: PopupReference, - @Inject(LOCAL_STORAGE) protected storageService: StorageService, - protected stateService: StateService + protected safeStorageService: SafeStorageService ) { } /** * @inheritDoc */ ngOnInit(): void { - this.stateService.getObservable().pipe( - tap((state) => { - this.storageEnabled.set(state.cookiesAccepted == true); + this.safeStorageService.observe().pipe( + tap(() => { + this.storageEnabled.set(this.safeStorageService.allowed()); - if (this.storageService.has(LoadAndSavePopupComponent.storageKey)) { - const saved = this.storageService.get(LoadAndSavePopupComponent.storageKey); + if (this.safeStorageService.has(LoadAndSavePopupComponent.storageKey)) { + const saved = this.safeStorageService.get(LoadAndSavePopupComponent.storageKey); this.saved.set(saved.reverse()); } } @@ -77,7 +74,7 @@ export class LoadAndSavePopupComponent implements OnInit { * Called when a user presses the save button. */ onSave() { - const saved = this.saved(); + const saved = this.saved().slice(); saved.push({ date: new Date(), @@ -123,14 +120,17 @@ export class LoadAndSavePopupComponent implements OnInit { * @param itemsToSave */ private save(itemsToSave: SavedCode[]) { - this.saved.set(itemsToSave); - if (itemsToSave.length == 0) { - this.storageService.remove(LoadAndSavePopupComponent.storageKey); + this.safeStorageService.clear(LoadAndSavePopupComponent.storageKey); return ; } - this.storageService.set(LoadAndSavePopupComponent.storageKey, itemsToSave); + try { + this.safeStorageService.save(LoadAndSavePopupComponent.storageKey, itemsToSave); + //this.saved.set(itemsToSave); + } catch (e) { + console.error('Cannot save code, storage is disabled.'); + } } } diff --git a/src/app/pages/playground/popups/platform-info/platform-info-popup.component.scss b/src/app/pages/playground/popups/platform-info/platform-info-popup.component.scss index 52b7039..eb54d5f 100644 --- a/src/app/pages/playground/popups/platform-info/platform-info-popup.component.scss +++ b/src/app/pages/playground/popups/platform-info/platform-info-popup.component.scss @@ -1,7 +1,7 @@ :host { @media screen and (min-width: 805px) { width: 750px !important; - min-height: 400px; + min-height: 470px; } article { diff --git a/src/app/pages/settings/settings.component.ts b/src/app/pages/settings/settings.component.ts index 4859995..c45d4d0 100644 --- a/src/app/pages/settings/settings.component.ts +++ b/src/app/pages/settings/settings.component.ts @@ -19,9 +19,9 @@ import { ChangeDetectionStrategy, Component, signal, WritableSignal } from '@angular/core'; import { CommonModule } from '@angular/common'; import { SwitchComponent } from '../../shared/components/switch/switch.component'; -import { StateService } from '../../shared/services/state.service'; import { tap } from 'rxjs'; import { Title } from '@angular/platform-browser'; +import { SafeStorageService } from '../../shared/services/safe-storage.service'; @Component({ selector: 'st-settings', @@ -42,19 +42,19 @@ export class SettingsComponent { /** * Constructor. * @param title - * @param stateService + * @param safeStorageService */ constructor( protected title: Title, - protected stateService: StateService, + protected safeStorageService: SafeStorageService, ) { this.title.setTitle('Settings - SYCL.tech'); - stateService.getObservable().pipe( + safeStorageService.observe().pipe( tap((state) => { - this.enableDarkMode.set(state.darkModeEnabled); - this.enableStorage.set(state.cookiesAccepted ? state.cookiesAccepted : false); - this.enableTracking.set(state.enableTracking); + this.enableStorage.set(state['st-cookies-accepted'] == true); + this.enableDarkMode.set(state['st-dark-mode-enabled'] == true); + this.enableTracking.set(state['st-enable-tracking'] == true); }) ).subscribe(); } @@ -63,10 +63,14 @@ export class SettingsComponent { * Called when a user changes any of the settings. */ onStateChanged() { - const state = this.stateService.snapshot(); - state.enableTracking = this.enableTracking(); - state.darkModeEnabled = this.enableDarkMode(); - state.cookiesAccepted = this.enableStorage(); - this.stateService.update(state); + if (!this.enableStorage()) { + // If storage is now disabled, clear any existing stored values + this.safeStorageService.clear(); + return ; + } + + this.safeStorageService.save('st-cookies-accepted', this.enableStorage(), false); + this.safeStorageService.save('st-dark-mode-enabled', this.enableDarkMode(), false); + this.safeStorageService.save('st-enable-tracking', this.enableTracking()); } } diff --git a/src/app/shared/services/safe-storage.service.ts b/src/app/shared/services/safe-storage.service.ts new file mode 100644 index 0000000..6760397 --- /dev/null +++ b/src/app/shared/services/safe-storage.service.ts @@ -0,0 +1,156 @@ +/*--------------------------------------------------------------------------------------------- + * + * Copyright (C) Codeplay Software Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + *--------------------------------------------------------------------------------------------*/ + +import { Inject, Injectable } from '@angular/core'; +import { LOCAL_STORAGE, StorageService } from 'ngx-webstorage-service'; +import { BehaviorSubject, Observable } from 'rxjs'; +import { environment } from '../../../environments/environment'; + +@Injectable({ + providedIn: 'root' +}) +export class SafeStorageService { + public static readonly STORAGE_ALLOWED_KEY = 'st-cookies-accepted'; + private readonly subject: BehaviorSubject; + private readonly allowedStorageKeys: string[] = environment.allowed_storage_keys; + + /** + * Constructor. + * @param storageService + */ + constructor( + @Inject(LOCAL_STORAGE) private storageService: StorageService + ) { + this.subject = new BehaviorSubject(this.state()); + } + + /** + * Determine if we are allowed to store data to the storage service. + */ + allowed(): boolean { + return this.storageService.has(SafeStorageService.STORAGE_ALLOWED_KEY) + && this.storageService.get(SafeStorageService.STORAGE_ALLOWED_KEY) == true; + } + + /** + * Return an observable, allowing changes to the state to be tracked. + */ + observe(): Observable { + return this.subject; + } + + /** + * Clear all the saved state, persisting if storage is allowed or not. + */ + clear(key?: string) { + if (key) { + this.storageService.remove(key); + } else { + this.storageService.clear(); + this.storageService.set(SafeStorageService.STORAGE_ALLOWED_KEY, this.allowed()); + } + + this.notify(); + } + + /** + * Save a value to the safe storage service. + * @param key the key to use to save and access the value + * @param value the value to store + * @param notify if we wish to notify subscribers that the state has changed + * @throws DefaultStorageKeys will be thrown if we are not allowed to store data + */ + save( + key: string, + value: any, + notify: boolean = true + ) { + if (key != SafeStorageService.STORAGE_ALLOWED_KEY) { + if (!this.allowed()) { + throw new StorageNotEnabledError(); + } + + if (!this.allowedStorageKeys.includes(key)) { + throw new KeyNotAllowedError(`The key "${key}" is not in the allowed key list.`); + } + } + + this.storageService.set(key, value); + + if (notify) { + this.notify(); + } + } + + /** + * Check if the safe storage service contains a value. + * @param key + */ + has( + key: string + ): boolean { + return this.storageService.has(key); + } + + /** + * Get a specific value using a key. Will return undefined if the key does not exist. + * @param key + */ + get( + key: string + ): any { + return this.storageService.get(key); + } + + /** + * Get the current state of all known allowed keys. + */ + state() { + const state: any = {}; + + for (const key of this.allowedStorageKeys) { + state[key] = this.storageService.get(key); + } + + return state; + } + + /** + * Notify any observers of the new updated storage state. + */ + private notify() { + this.subject.next(this.state()); + } +} + +/** + * State interface. + */ +export interface State { + [Key: string]: any; +} + +/** + * Thrown when we are not allowed to store data. + */ +export class StorageNotEnabledError extends Error {} + +/** + * Thrown when there is an attempt to store a value when it's not listed in the allowed key list. + */ +export class KeyNotAllowedError extends Error {} diff --git a/src/app/shared/services/state.service.ts b/src/app/shared/services/state.service.ts deleted file mode 100644 index e369d44..0000000 --- a/src/app/shared/services/state.service.ts +++ /dev/null @@ -1,120 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * - * Copyright (C) Codeplay Software Ltd. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - *--------------------------------------------------------------------------------------------*/ - -import { Inject, Injectable } from '@angular/core'; -import { LOCAL_STORAGE, StorageService } from 'ngx-webstorage-service'; -import { BehaviorSubject, Observable } from 'rxjs'; - -@Injectable({ - providedIn: 'root' -}) -export class StateService { - protected stateSubject: BehaviorSubject; - - /** - * Constructor. - * @param storageService - */ - constructor( - @Inject(LOCAL_STORAGE) private storageService: StorageService - ) { - const applicationState: ApplicationState = { - cookiesAccepted: this.getStorageServiceValue('st-cookies-accepted'), - darkModeEnabled: this.getStorageServiceValue('st-dark-mode-enabled', false), - enableTracking: this.getStorageServiceValue('st-enable-tracking', true) - } - - this.stateSubject = new BehaviorSubject(applicationState); - } - - getStorageServiceValue(key: string, defaultValue?: any) { - if (!this.storageService.has(key)) { - return defaultValue; - } - - const value = this.storageService.get(key); - - if (value == undefined || value == 'undefined') { - return defaultValue; - } - - return value; - } - - /** - * Get the state observable that can be subscribed to for update changes. - */ - getObservable(): Observable { - return this.stateSubject; - } - - /** - * Get the latest state snapshot. Avoid using this in reactive circumstances. - */ - snapshot() { - return this.stateSubject.value; - } - - /** - * Set the dark mode state. - * @param enabled - */ - setDarkMode(enabled: boolean) { - const state = this.snapshot(); - state.darkModeEnabled = enabled; - this.update(state); - } - - /** - * Set the storage acceptance policy. - * @param enabled - */ - setStoragePolicyAccepted(enabled: boolean | undefined) { - const state = this.snapshot(); - state.cookiesAccepted = enabled; - this.update(state); - } - - /** - * Update the state. - * @param state - */ - update(state: ApplicationState) { - if (!state.cookiesAccepted) { - this.storageService.clear(); - this.storageService.set('st-cookies-accepted', state.cookiesAccepted); - this.stateSubject.next(state); - return ; - } - - this.storageService.set('st-cookies-accepted', state.cookiesAccepted); - this.storageService.set('st-dark-mode-enabled', state.darkModeEnabled); - this.storageService.set('st-enable-tracking', state.enableTracking); - - this.stateSubject.next(state); - } -} - -/** - * Interface that represents the state of the application. - */ -export interface ApplicationState { - cookiesAccepted: boolean | undefined - darkModeEnabled: boolean - enableTracking: boolean -} diff --git a/src/environments/environment.ts b/src/environments/environment.ts index f890f3d..7358879 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -27,5 +27,13 @@ export const environment = { discord: 'https://discord.com/invite/FkGSFA3asN' }, privacy_policy_email: 'info@codeplay.com', - fathom_analytics_token: 'MMWGQHXZ' + fathom_analytics_token: 'MMWGQHXZ', + allowed_storage_keys: [ + 'st-cookies-accepted', + 'st-dark-mode-enabled', + 'st-enable-tracking', + 'st-playground-compiler-tag', + 'st-playground-saved', + 'st-last-visit-date' + ] }; From 6bc73b22ef715a7ec86e1e276a17d29747b355e2 Mon Sep 17 00:00:00 2001 From: Scott Straughan Date: Wed, 25 Sep 2024 16:32:45 +0100 Subject: [PATCH 02/27] UI tweaks to popups. --- src/app/shared/components/popup/layouts/common.scss | 2 +- src/app/shared/components/popup/layouts/widget.scss | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/app/shared/components/popup/layouts/common.scss b/src/app/shared/components/popup/layouts/common.scss index a473b67..1ab9a81 100644 --- a/src/app/shared/components/popup/layouts/common.scss +++ b/src/app/shared/components/popup/layouts/common.scss @@ -28,6 +28,6 @@ a.button { background-color: var(--color-blue); &.cancel { - background-color: var(--color-hint-sixth); + background-color: var(--color-grey); } } diff --git a/src/app/shared/components/popup/layouts/widget.scss b/src/app/shared/components/popup/layouts/widget.scss index cff3497..c7c9ff6 100644 --- a/src/app/shared/components/popup/layouts/widget.scss +++ b/src/app/shared/components/popup/layouts/widget.scss @@ -3,6 +3,12 @@ :host { > header { padding: 0; + color: var(--text-color); + background: linear-gradient(325deg, rgb(255, 61, 0) 0%, rgb(228, 228, 228) 57%); + + :host-context(.dark-mode) & { + background: var(--color-orange); + } .title { flex: 1; @@ -17,6 +23,7 @@ } } + .author { width: 100%; padding: 1rem 3rem; From b40dcc1dda4462453f80f4752161d4765e714708 Mon Sep 17 00:00:00 2001 From: Scott Straughan Date: Wed, 25 Sep 2024 16:33:19 +0100 Subject: [PATCH 03/27] Added new "changed" page that allows users to quickly see what has been updated/changed on SYCL.tech. --- src/app/app.routes.ts | 5 + .../change-start-date.component.html | 30 ++ .../change-start-date.component.scss | 37 +++ .../change-start-date.component.ts | 106 +++++++ src/app/pages/changed/changed.component.html | 160 ++++++++++ src/app/pages/changed/changed.component.scss | 137 +++++++++ src/app/pages/changed/changed.component.ts | 275 ++++++++++++++++++ .../shared/services/models/event.service.ts | 18 ++ .../shared/services/models/news.service.ts | 18 ++ .../services/models/playground.service.ts | 35 +-- .../shared/services/models/project.service.ts | 18 ++ .../services/models/research.service.ts | 17 ++ .../shared/services/models/videos.service.ts | 17 ++ 13 files changed, 851 insertions(+), 22 deletions(-) create mode 100644 src/app/pages/changed/change-start-date-popup/change-start-date.component.html create mode 100644 src/app/pages/changed/change-start-date-popup/change-start-date.component.scss create mode 100644 src/app/pages/changed/change-start-date-popup/change-start-date.component.ts create mode 100644 src/app/pages/changed/changed.component.html create mode 100644 src/app/pages/changed/changed.component.scss create mode 100644 src/app/pages/changed/changed.component.ts diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index f5c2a8d..2f7b16c 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -36,6 +36,7 @@ import { CookiesComponent } from './pages/cookies/cookies.component'; import { PrivacyComponent } from './pages/privacy/privacy.component'; import { SettingsComponent } from './pages/settings/settings.component'; import { Error404Component } from './pages/404/error-404.component'; +import { ChangedComponent } from './pages/changed/changed.component'; export const routes: Routes = [ { @@ -47,6 +48,10 @@ export const routes: Routes = [ path: 'playground', component: PlaygroundComponent }, + { + path: 'changed', + component: ChangedComponent + }, { path: 'getting-started', component: GettingStartedComponent diff --git a/src/app/pages/changed/change-start-date-popup/change-start-date.component.html b/src/app/pages/changed/change-start-date-popup/change-start-date.component.html new file mode 100644 index 0000000..2225e6f --- /dev/null +++ b/src/app/pages/changed/change-start-date-popup/change-start-date.component.html @@ -0,0 +1,30 @@ +
+
+ event +

Set Start Date

+ + @if (noLastVisit()) { +

You've not been here before.

+ } + +

Please set a start date that we can use to determine what has changed.

+
+
+ +
+
+
+ +
+ @if (errorMessage(); as message) { +
+ } +
+ +
diff --git a/src/app/pages/changed/change-start-date-popup/change-start-date.component.scss b/src/app/pages/changed/change-start-date-popup/change-start-date.component.scss new file mode 100644 index 0000000..7b21e42 --- /dev/null +++ b/src/app/pages/changed/change-start-date-popup/change-start-date.component.scss @@ -0,0 +1,37 @@ +:host { + @media screen and (min-width: 800px) { + width: 100vw !important; + } + + header { + h2 { + font-weight: normal; + opacity: .7; + } + + div { + text-align: center; + } + } + + input { + display: block; + width: 100%; + padding: 1.5rem; + border: var(--border-default); + border-radius: var(--border-radius); + background-color: transparent; + color: var(--text-color); + font-family: inherit; + font-size: 1.2rem; + } + + .error { + background-color: #ffd2d2; + padding: 2rem; + text-align: center; + color: red; + margin-top: 1rem; + border-radius: var(--border-radius); + } +} \ No newline at end of file diff --git a/src/app/pages/changed/change-start-date-popup/change-start-date.component.ts b/src/app/pages/changed/change-start-date-popup/change-start-date.component.ts new file mode 100644 index 0000000..20b6da6 --- /dev/null +++ b/src/app/pages/changed/change-start-date-popup/change-start-date.component.ts @@ -0,0 +1,106 @@ +/*--------------------------------------------------------------------------------------------- + * + * Copyright (C) Codeplay Software Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + *--------------------------------------------------------------------------------------------*/ + +import { ChangeDetectionStrategy, Component, Inject, signal, Signal, WritableSignal } from '@angular/core'; +import { DatePipe, NgOptimizedImage } from '@angular/common'; +import { PopupReference } from '../../../shared/components/popup/popup.service'; + +@Component({ + selector: 'st-change-start-date', + standalone: true, + templateUrl: './change-start-date.component.html', + imports: [ + NgOptimizedImage, + DatePipe + ], + styleUrls: [ + '../../../shared/components/popup/layouts/large-top-header.scss', + './change-start-date.component.scss' + ], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class ChangeStartDateComponent { + protected readonly noLastVisit: Signal; + protected readonly errorMessage: WritableSignal = signal(''); + protected readonly startDate: Signal; + + /** + * Constructor. + * @param popupReference + */ + constructor( + @Inject('POPUP_DATA') protected popupReference: PopupReference, + ) { + if (this.popupReference.data) { + this.noLastVisit = signal(false); + this.startDate = signal(new Date(this.popupReference.data)); + } else { + this.noLastVisit = signal(true); + + const dateDateTime = new Date(); + dateDateTime.setMonth(dateDateTime.getMonth() - 5); + dateDateTime.setDate(1); + + // Set a default date, just in case we can't load one successfully + this.startDate = signal(dateDateTime); + } + } + + /** + * Called when the date selector changes. + * @param event + */ + onDateChanged(event: any) { + const selectedDate: any = new Date(event.target.value); + const currentDate = new Date(); + + if (isNaN(selectedDate)) { + this.errorMessage.set('Please ensure that the date is in a correct format.'); + return; + } + + if (ChangeStartDateComponent.monthDifference(selectedDate, currentDate) > 6) { + this.errorMessage.set('You cannot pick a date that is more than six months from the current date.'); + return; + } + + if (selectedDate > currentDate) { + this.errorMessage.set('You cannot set a date that is in the future, please choose an earlier date.'); + return; + } + + this.popupReference.close(selectedDate); + } + + /** + * Called when the user clicks on the cancel button. + */ + onClose() { + this.popupReference.close(this.startDate()); + } + + /** + * Calculate the month difference between two dates. + * @param dateFrom + * @param dateTo + */ + public static monthDifference(dateFrom: Date, dateTo: Date): number { + return dateTo.getMonth() - dateFrom.getMonth() + + (12 * (dateTo.getFullYear() - dateFrom.getFullYear())) + } +} diff --git a/src/app/pages/changed/changed.component.html b/src/app/pages/changed/changed.component.html new file mode 100644 index 0000000..000704a --- /dev/null +++ b/src/app/pages/changed/changed.component.html @@ -0,0 +1,160 @@ +
+
+
+
+

What's Changed?

+

Quickly see what content has been added to SYCL.tech since your last visit on + {{ currentDate() | date: 'mediumDate' }}.

+ + +
+
+

My Last Visit

+

{{ startDate() | date: 'mediumDate' }}

+
+
+ arrow_circle_right +
+
+

Today

+

{{ currentDate() | date: 'mediumDate' }}

+
+
+ + + +
+
+
+ newspaper +
+
+
+ +@if (newsLoadingState() != LoadingState.NOT_STARTED) { + +
+
+ @if (newsLoadingState() == LoadingState.LOAD_SUCCESS) { +
+
+
+

News & Updates

+

We have had {{ updatedNews().length }} new additions since + {{ startDate() | date: 'mediumDate' }}

+
+
+
+ @if (updatedNews().length > 0) { + + newspaperView All + } +
+
+
+ @for (item of updatedNews(); track item.id) { + + } +
+ } @else if(newsLoadingState() == LoadingState.LOADING) { + + } +
+
+} + +@if (projectsLoadingState() != LoadingState.NOT_STARTED) { + +
+
+ @if (projectsLoadingState() == LoadingState.LOAD_SUCCESS) { +
+
+
+

Projects

+

We have had {{ updatedProjects().length }} new additions since + {{ startDate() | date: 'mediumDate' }}

+
+
+
+ @if (updatedProjects().length > 0) { + + geneticsView All + } +
+
+
+ @for (item of updatedProjects(); track item.id) { + + } +
+ } @else if(projectsLoadingState() == LoadingState.LOADING) { + + } +
+
+} + +@if (videoLoadingState() != LoadingState.NOT_STARTED) { + +
+
+ @if (videoLoadingState() == LoadingState.LOAD_SUCCESS) { +
+
+
+

Videos

+

We have had {{ updatedVideos().length }} new additions since + {{ startDate() | date: 'mediumDate' }}

+
+
+
+ @if (updatedVideos().length > 0) { + + slideshowView All + } +
+
+
+ @for (item of updatedVideos(); track item.id) { + + } +
+ } @else if(videoLoadingState() == LoadingState.LOADING) { + + } +
+
+} + +@if (researchLoadingState() != LoadingState.NOT_STARTED) { + +
+
+ @if (researchLoadingState() == LoadingState.LOAD_SUCCESS) { +
+
+
+

Research Papers

+

We have had {{ updatedResearch().length }} new additions since + {{ startDate() | date: 'mediumDate' }}

+
+
+
+ @if (updatedResearch().length > 0) { + + biotechView All + } +
+
+
+ @for (item of updatedResearch(); track item.id) { + + } +
+ } @else if(researchLoadingState() == LoadingState.LOADING) { + + } +
+
+} diff --git a/src/app/pages/changed/changed.component.scss b/src/app/pages/changed/changed.component.scss new file mode 100644 index 0000000..b98c23c --- /dev/null +++ b/src/app/pages/changed/changed.component.scss @@ -0,0 +1,137 @@ +/** + * Styling for all sections other than intro. + */ +section:not(#intro) { + header { + padding-bottom: 0; + + h1 { + margin-bottom: .5rem; + } + + h2 { + font-size: 1.2rem; + margin: 0; + opacity: .7; + } + } + + .list { + padding-top: 1rem; + display: grid; + grid-template-columns: 1fr; + grid-gap: 1rem; + + @media screen and (min-width: 1000px) { + grid-template-columns: 1fr 1fr; + } + } +} + +/** + * Styling for the introduction panel. + */ +section#intro { + .panel { + background: linear-gradient(90deg, rgb(0 0 33) 35%, rgb(255 61 0) 100%); + } + + header h1 { + margin-bottom: 0; + } + + .date-range { + font-size: 1.5rem; + display: flex; + border: rgba(255, 255, 255, 0.26) 1px solid; + border-radius: var(--border-radius); + + & > div { + border-right: rgba(255, 255, 255, 0.26) 1px solid; + padding: .8rem 1rem; + cursor: default; + + &:last-of-type { + border-right: 0; + } + + &.date { + transition: var(--transition-fast); + display: block; + + &.selectable { + cursor: pointer; + + &:hover { + background-color: rgba(255, 255, 255, .1); + } + } + + h2 { + font-size: 1.4rem; + opacity: .9; + margin: 0; + } + + h3 { + display: block; + font-size: 1rem; + opacity: .6; + font-weight: normal; + margin: .2rem 0 0 0; + } + } + + &.sep { + display: flex; + align-items: center; + + span { + font-size: 30px; + } + } + } + } + + st-quick-links { + margin-top: 1rem; + } +} + +/** + * Styling for the projects update section. + */ +section#projects { + st-project-widget { + border: var(--border-default); + border-radius: var(--border-radius); + } +} + +/** + * Styling for the video update section. + */ +section#videos { + st-video-widget { + border: var(--border-default); + border-radius: var(--border-radius); + } +} + +/** + * Styling for the research update section. + */ +section#research { + st-research-widget { + border: var(--border-default); + border-radius: var(--border-radius); + } + + .list { + grid-template-columns: 1fr; + + @media screen and (min-width: 1100px) { + grid-template-columns: 1fr 1fr 1fr; + } + } +} \ No newline at end of file diff --git a/src/app/pages/changed/changed.component.ts b/src/app/pages/changed/changed.component.ts new file mode 100644 index 0000000..0d5fafd --- /dev/null +++ b/src/app/pages/changed/changed.component.ts @@ -0,0 +1,275 @@ +/*--------------------------------------------------------------------------------------------- + * + * Copyright (C) Codeplay Software Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + *--------------------------------------------------------------------------------------------*/ + +import { + ChangeDetectionStrategy, + Component, + computed, + OnDestroy, + OnInit, + Signal, + signal, + WritableSignal +} from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { Title } from '@angular/platform-browser'; +import { NewsService } from '../../shared/services/models/news.service'; +import { NewsModel } from '../../shared/models/news.model'; +import { NewsWidgetComponent, NewsWidgetLayout } from '../news/shared/news-widget/news-widget.component'; +import { RouterLink } from '@angular/router'; +import { ProjectModel } from '../../shared/models/project.model'; +import { ProjectService } from '../../shared/services/models/project.service'; +import { VideosService } from '../../shared/services/models/videos.service'; +import { ResearchService } from '../../shared/services/models/research.service'; +import { ResearchModel } from '../../shared/models/research.model'; +import { VideoModel } from '../../shared/models/video.model'; +import { + ProjectWidgetComponent, + ProjectWidgetLayout +} from '../ecosystem/projects/shared/project-widget/project-widget.component'; +import { VideoWidgetComponent } from '../ecosystem/videos/video-widget/video-widget.component'; +import { + ResearchWidgetComponent, + ResearchWidgetLayout +} from '../ecosystem/research/shared/research-widget/research-widget.component'; +import { + QuickLink, + QuickLinksComponent, + QuickLinkType +} from '../../shared/components/quick-links/quick-links.component'; +import { PopupService } from '../../shared/components/popup/popup.service'; +import { ChangeStartDateComponent } from './change-start-date-popup/change-start-date.component'; +import { catchError, firstValueFrom, of, tap } from 'rxjs'; +import { LoadingState } from '../../shared/LoadingState'; +import { LoadingComponent } from '../../shared/components/loading/loading.component'; +import { SafeStorageService } from '../../shared/services/safe-storage.service'; +import { PlatformService } from '../../shared/services/platform.service'; + +@Component({ + selector: 'st-changed', + standalone: true, + imports: [ + CommonModule, + NewsWidgetComponent, + RouterLink, + ProjectWidgetComponent, + VideoWidgetComponent, + ResearchWidgetComponent, + QuickLinksComponent, + LoadingComponent + ], + templateUrl: './changed.component.html', + styleUrl: './changed.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class ChangedComponent implements OnInit, OnDestroy { + protected static readonly STORAGE_LAST_VISIT = 'st-last-visit-date'; + + protected readonly startDate: WritableSignal = signal(undefined); + protected readonly currentDate: WritableSignal = signal(new Date()); + protected readonly quickLinks: Signal; + + protected readonly newsLoadingState: WritableSignal = signal(LoadingState.NOT_STARTED); + protected readonly projectsLoadingState: WritableSignal = signal(LoadingState.NOT_STARTED); + protected readonly videoLoadingState: WritableSignal = signal(LoadingState.NOT_STARTED); + protected readonly researchLoadingState: WritableSignal = signal(LoadingState.NOT_STARTED); + + protected readonly updatedNews: WritableSignal = signal([]); + protected readonly updatedProjects: WritableSignal = signal([]); + protected readonly updatedVideos: WritableSignal = signal([]); + protected readonly updatedResearch: WritableSignal = signal([]); + + protected readonly NewsWidgetLayout = NewsWidgetLayout; + protected readonly ProjectWidgetLayout = ProjectWidgetLayout; + protected readonly ResearchWidgetLayout = ResearchWidgetLayout; + protected readonly LoadingState = LoadingState; + + protected saveTimer: any = undefined; + + /** + * Constructor. + * @param title + * @param platformService + * @param safeStorageService + * @param popupService + * @param projectService + * @param newsService + * @param researchService + * @param videosService + */ + constructor( + protected title: Title, + protected platformService: PlatformService, + protected safeStorageService: SafeStorageService, + protected popupService: PopupService, + protected projectService: ProjectService, + protected newsService: NewsService, + protected researchService : ResearchService, + protected videosService: VideosService, + ) { + this.title.setTitle('Digest - SYCL.tech'); + + // Compute the quick links signal + this.quickLinks = computed(() => { + // Escape early if we haven't started loading yet + if (this.newsLoadingState() == LoadingState.NOT_STARTED) { + return []; + } + + return [ + { + name: `News (${this.updatedNews().length})`, + url: 'news', + type: QuickLinkType.FRAGMENT + }, + { + name: `Projects (${this.updatedProjects().length})`, + url: 'projects', + type: QuickLinkType.FRAGMENT + }, + { + name: `Videos (${this.updatedVideos().length})`, + url: 'videos', + type: QuickLinkType.FRAGMENT + }, + { + name: `Research Papers (${this.updatedResearch().length})`, + url: 'research', + type: QuickLinkType.FRAGMENT + } + ]; + }); + } + + /** + * @inheritdoc + */ + ngOnInit() { + if (this.platformService.isClient()) { + if (this.safeStorageService.has(ChangedComponent.STORAGE_LAST_VISIT)) { + const lastVisitDate = new Date(this.safeStorageService.get(ChangedComponent.STORAGE_LAST_VISIT)); + this.startDate.set(lastVisitDate); + this.reload(); + } else { + this.onDateSelectorClicked(); + this.saveLastVisit(); + } + + this.saveTimer = setInterval(() => { + console.log('Automatically updating last visit time.'); + this.saveLastVisit(); + }, 6000); + } + } + + /** + * @inheritdoc + */ + ngOnDestroy() { + this.stopAutosave(); + } + + /** + * Stop any autosave timers. + */ + stopAutosave() { + if (this.saveTimer) { + clearTimeout(this.saveTimer); + } + } + + /** + * Reload all the content. + */ + reload() { + const startDate = this.startDate(); + + if (!startDate) { + return; + } + + ChangedComponent.populate( + this.projectService, this.projectsLoadingState, this.updatedProjects, startDate); + + ChangedComponent.populate( + this.newsService, this.newsLoadingState, this.updatedNews, startDate); + + ChangedComponent.populate( + this.researchService, this.researchLoadingState, this.updatedResearch, startDate); + + ChangedComponent.populate( + this.videosService, this.videoLoadingState, this.updatedVideos, startDate); + } + + /** + * Called when a user clicks on the start date, if they want to change the date. + */ + onDateSelectorClicked() { + firstValueFrom(this.popupService.create(ChangeStartDateComponent, this.startDate(), true).onChanged) + .then( + (date) => { + if (date) { + this.stopAutosave(); + this.startDate.set(date); + this.reload(); + this.saveLastVisit(date); + } + } + ); + } + + /** + * Update the last visit date in storage. + */ + saveLastVisit(date?: Date) { + try { + if (!date) { + date = new Date(); + } + + this.safeStorageService.save(ChangedComponent.STORAGE_LAST_VISIT, date); + } catch (e) { + console.log(e); + } + } + + /** + * Populate results from the provided services, updating loading states etc. + * @param service + * @param loadingState + * @param resultSignal + * @param startDate + */ + static populate( + service: any, + loadingState: WritableSignal, + resultSignal: WritableSignal, + startDate: Date + ) { + firstValueFrom( + service.afterDate(startDate) + .pipe( + tap(() => loadingState.set(LoadingState.LOAD_SUCCESS)), + catchError((error) => { + loadingState.set(LoadingState.LOAD_FAILURE); + return of(error) + }) + ) + ).then((items) => resultSignal.set(items)) + } +} diff --git a/src/app/shared/services/models/event.service.ts b/src/app/shared/services/models/event.service.ts index 24dabc9..5b87284 100644 --- a/src/app/shared/services/models/event.service.ts +++ b/src/app/shared/services/models/event.service.ts @@ -23,6 +23,7 @@ import { ContributorService } from './contributor.service'; import { JsonFeedService } from '../json-feed.service'; import { FilterGroup } from '../../managers/ResultFilterManager'; import { map, Observable, of } from 'rxjs'; +import { NewsModel } from '../../models/news.model'; @Injectable({ providedIn: 'root' @@ -126,4 +127,21 @@ export class EventService extends JsonFeedService { }) ); } + + /** + * Get all event items after a specific date. + * @param startDate + * @param limit + */ + afterDate( + startDate: Date, + limit: number | null = null + ): Observable { + return this.all(limit) + .pipe( + map((items) => { + return startDate ? items.filter((item) => (item.starts >= startDate)) : items; + }) + ); + } } diff --git a/src/app/shared/services/models/news.service.ts b/src/app/shared/services/models/news.service.ts index e0eece4..80bb2bc 100644 --- a/src/app/shared/services/models/news.service.ts +++ b/src/app/shared/services/models/news.service.ts @@ -24,6 +24,7 @@ import { NewsModel } from '../../models/news.model'; import { JsonFeedService } from '../json-feed.service'; import { map, Observable, of } from 'rxjs'; import { PinnedModel } from '../../models/pinned.model'; +import { VideoModel } from '../../models/video.model'; @Injectable({ providedIn: 'root' @@ -148,4 +149,21 @@ export class NewsService extends JsonFeedService { ): Observable { return super._all(limit, offset, filterGroups).pipe(map((f => f.items))); } + + /** + * Get all news items after a specific date. + * @param startDate + * @param limit + */ + afterDate( + startDate: Date, + limit: number | null = null + ): Observable { + return this.all(limit) + .pipe( + map((items) => { + return startDate ? items.filter((item) => (item.date >= startDate)) : items; + }) + ); + } } diff --git a/src/app/shared/services/models/playground.service.ts b/src/app/shared/services/models/playground.service.ts index 3687594..d790bb1 100644 --- a/src/app/shared/services/models/playground.service.ts +++ b/src/app/shared/services/models/playground.service.ts @@ -16,15 +16,13 @@ * *--------------------------------------------------------------------------------------------*/ -import { effect, Inject, Injectable, signal, WritableSignal } from '@angular/core'; +import { Injectable, signal, WritableSignal } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { DomSanitizer } from '@angular/platform-browser'; import { BehaviorSubject, catchError, map, Observable, of } from 'rxjs'; import { CompilationResultModel } from '../../models/compilation-result.model'; import { PlaygroundSampleModel } from '../../models/playground-sample.model'; -import { LOCAL_STORAGE, StorageService } from 'ngx-webstorage-service'; -import { StateService } from '../state.service'; - +import { SafeStorageService } from '../safe-storage.service'; import Convert from 'ansi-to-html'; @Injectable({ @@ -57,19 +55,17 @@ export class PlaygroundService { * Constructor. * @param httpClient * @param domSanitizer - * @param stateService - * @param storageService + * @param safeStorageService */ constructor( protected httpClient: HttpClient, protected domSanitizer: DomSanitizer, - protected stateService: StateService, - @Inject(LOCAL_STORAGE) protected storageService: StorageService + protected safeStorageService: SafeStorageService ) { this.sampleSubject = new BehaviorSubject(undefined); - if (this.storageService.has(PlaygroundService.COMPILER_STORAGE_KEY)) { - const compilerTag = this.storageService.get(PlaygroundService.COMPILER_STORAGE_KEY); + if (this.safeStorageService.has(PlaygroundService.COMPILER_STORAGE_KEY)) { + const compilerTag = this.safeStorageService.get(PlaygroundService.COMPILER_STORAGE_KEY); for (const compiler of this.supportedCompilers) { if (compiler.tag == compilerTag) { @@ -78,17 +74,6 @@ export class PlaygroundService { } } } - - /** - * This effect will update the storage service with the selected compiler - */ - effect(() => { - const selectedCompiler = this.selectedCompiler(); - - if (this.stateService.snapshot().cookiesAccepted) { - this.storageService.set('st-playground-compiler-tag', selectedCompiler.tag); - } - }) } /** @@ -197,6 +182,12 @@ export class PlaygroundService { */ setCompiler(compiler: PlaygroundCompiler) { this.selectedCompiler.set(compiler); + + try { + this.safeStorageService.save('st-playground-compiler-tag', compiler.tag); + } catch (e) { + console.error('Cannot save compiler choice, storage is disabled.'); + } } /** @@ -472,7 +463,7 @@ export class OneApiCompiler implements PlaygroundCompiler { */ export class AdaptiveCppCompiler implements PlaygroundCompiler { public name = 'AdaptiveCpp'; - public enabled: boolean = false; + public enabled: boolean = true; public logo: string = '/assets/images/ecosystem/implementations/adaptivecpp/logo-black.webp'; public tag = 'adaptive' public flags = '-fsycl -g0 -Rno-debug-disables-optimization'; diff --git a/src/app/shared/services/models/project.service.ts b/src/app/shared/services/models/project.service.ts index e1b7142..83033ca 100644 --- a/src/app/shared/services/models/project.service.ts +++ b/src/app/shared/services/models/project.service.ts @@ -25,6 +25,7 @@ import { ContributorModel } from '../../models/contributor.model'; import { JsonFeedService } from '../json-feed.service'; import { map, Observable, of } from 'rxjs'; import { MarkdownService } from 'ngx-markdown'; +import { NewsModel } from '../../models/news.model'; @Injectable({ providedIn: 'root' @@ -147,6 +148,23 @@ export class ProjectService extends JsonFeedService { ); } + /** + * Get all project items after a specific date. + * @param startDate + * @param limit + */ + afterDate( + startDate: Date, + limit: number | null = null + ): Observable { + return this.all(limit) + .pipe( + map((items) => { + return startDate ? items.filter((item) => (item.date_created >= startDate)) : items; + }) + ); + } + /** * Create a wrapper repo contributor. * @param name diff --git a/src/app/shared/services/models/research.service.ts b/src/app/shared/services/models/research.service.ts index 4f4100a..a8d43d5 100644 --- a/src/app/shared/services/models/research.service.ts +++ b/src/app/shared/services/models/research.service.ts @@ -81,4 +81,21 @@ export class ResearchService extends JsonFeedService { map((items) => items[Math.floor(Math.random() * items.length)]) ); } + + /** + * Get all research items after a specific date. + * @param startDate + * @param limit + */ + afterDate( + startDate: Date, + limit: number | null = null + ): Observable { + return this.all(limit) + .pipe( + map((items) => { + return startDate ? items.filter((item) => (item.date >= startDate)) : items; + }) + ); + } } diff --git a/src/app/shared/services/models/videos.service.ts b/src/app/shared/services/models/videos.service.ts index bf0a921..3fdc724 100644 --- a/src/app/shared/services/models/videos.service.ts +++ b/src/app/shared/services/models/videos.service.ts @@ -85,6 +85,23 @@ export class VideosService extends JsonFeedService { ); } + /** + * Get all video items after a specific date. + * @param startDate + * @param limit + */ + afterDate( + startDate: Date, + limit: number | null = null + ): Observable { + return this.all(limit) + .pipe( + map((items) => { + return startDate ? items.filter((item) => (item.date >= startDate)) : items; + }) + ); + } + /** * Attempt to generate an embed URL based on the external provider. * @param externalUrl From 251fedb36b41f66834597b27c093893f93dbf1e5 Mon Sep 17 00:00:00 2001 From: Scott Straughan Date: Wed, 25 Sep 2024 16:33:27 +0100 Subject: [PATCH 04/27] UI fix. --- .../ecosystem/videos/video-widget/video-widget.component.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/pages/ecosystem/videos/video-widget/video-widget.component.scss b/src/app/pages/ecosystem/videos/video-widget/video-widget.component.scss index 136e251..c305f7f 100644 --- a/src/app/pages/ecosystem/videos/video-widget/video-widget.component.scss +++ b/src/app/pages/ecosystem/videos/video-widget/video-widget.component.scss @@ -114,7 +114,6 @@ .footer { background: linear-gradient(94deg, rgba(0, 0, 0, .05) 0%, rgba(239, 239, 239, 0) 100%); padding: .5rem 2rem; - color: rgba(0, 0, 0, .5); text-align: left; font-size: .8rem; cursor: default; From f34d35f40eb913eca8233197cf88c4751b23a2cd Mon Sep 17 00:00:00 2001 From: Scott Straughan Date: Wed, 25 Sep 2024 16:33:45 +0100 Subject: [PATCH 05/27] Small bug fix. --- src/app/shared/components/switch/switch.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/shared/components/switch/switch.component.ts b/src/app/shared/components/switch/switch.component.ts index db64e16..4f03c4c 100644 --- a/src/app/shared/components/switch/switch.component.ts +++ b/src/app/shared/components/switch/switch.component.ts @@ -44,12 +44,12 @@ export class SwitchComponent { */ @HostListener('click', ['$event']) onClick() { - this.clicked.emit(); - if (!this.enabled()) { + this.clicked.emit(); return ; } this.checked.set(!this.checked()); + this.clicked.emit(); } } From a555796cb4d4b35df126ff62f8c8cc4c3a7df064 Mon Sep 17 00:00:00 2001 From: Scott Straughan Date: Wed, 25 Sep 2024 16:41:23 +0100 Subject: [PATCH 06/27] Slightly better messaging. --- src/app/pages/changed/changed.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/pages/changed/changed.component.ts b/src/app/pages/changed/changed.component.ts index 0d5fafd..4d60cf1 100644 --- a/src/app/pages/changed/changed.component.ts +++ b/src/app/pages/changed/changed.component.ts @@ -171,7 +171,6 @@ export class ChangedComponent implements OnInit, OnDestroy { } this.saveTimer = setInterval(() => { - console.log('Automatically updating last visit time.'); this.saveLastVisit(); }, 6000); } @@ -243,8 +242,9 @@ export class ChangedComponent implements OnInit, OnDestroy { } this.safeStorageService.save(ChangedComponent.STORAGE_LAST_VISIT, date); + console.log('Successfully updated last visit time.'); } catch (e) { - console.log(e); + console.error('Skipping saving last visit time due to error.'); } } From 9be3b6699fbf8a82dba21b84124e509d1c3fda52 Mon Sep 17 00:00:00 2001 From: Scott Straughan Date: Wed, 25 Sep 2024 16:42:42 +0100 Subject: [PATCH 07/27] Tidied up the messaging slightly. --- src/app/pages/changed/changed.component.html | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/app/pages/changed/changed.component.html b/src/app/pages/changed/changed.component.html index 000704a..e3d4ebc 100644 --- a/src/app/pages/changed/changed.component.html +++ b/src/app/pages/changed/changed.component.html @@ -3,8 +3,7 @@

What's Changed?

-

Quickly see what content has been added to SYCL.tech since your last visit on - {{ currentDate() | date: 'mediumDate' }}.

+

Quickly see what content has been added since your last visit.

From e69b8c78d5cd25b77e232bd988365194b3e166d5 Mon Sep 17 00:00:00 2001 From: Scott Straughan Date: Thu, 26 Sep 2024 19:26:10 +0100 Subject: [PATCH 08/27] Tweaks to cookie page to add missing cookies and tweak table ui. --- src/app/pages/cookies/cookies.component.html | 17 ++++++++++++++++- src/app/pages/cookies/cookies.component.scss | 6 ++++++ src/app/pages/cookies/cookies.component.ts | 4 +++- .../cookie-acceptance.component.ts | 1 + 4 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/app/pages/cookies/cookies.component.html b/src/app/pages/cookies/cookies.component.html index bd201f5..6bed8d5 100644 --- a/src/app/pages/cookies/cookies.component.html +++ b/src/app/pages/cookies/cookies.component.html @@ -29,7 +29,7 @@

What We Store

Cookie Name - Whats It For? + Description Type @@ -69,6 +69,21 @@

What We Store

1st Party Cookie
+ + st-last-visit-date + Used to track the last time you visited the site, used on the changed + page. + +
1st Party Cookie
+ + + + st-blocked-alerts + Used to hide any alerts. + +
1st Party Cookie
+ +
diff --git a/src/app/pages/cookies/cookies.component.scss b/src/app/pages/cookies/cookies.component.scss index 8b012e1..32d0270 100644 --- a/src/app/pages/cookies/cookies.component.scss +++ b/src/app/pages/cookies/cookies.component.scss @@ -1,5 +1,7 @@ table { width: 100%; + border-radius: var(--border-radius); + overflow: hidden; td, th { padding: 1.2rem; @@ -15,4 +17,8 @@ table { } } } + + tr td:first-of-type { + font-weight: bold; + } } diff --git a/src/app/pages/cookies/cookies.component.ts b/src/app/pages/cookies/cookies.component.ts index a6fc631..485ddaf 100644 --- a/src/app/pages/cookies/cookies.component.ts +++ b/src/app/pages/cookies/cookies.component.ts @@ -22,12 +22,14 @@ import { environment } from '../../../environments/environment'; import { Title } from '@angular/platform-browser'; import { SafeStorageService } from '../../shared/services/safe-storage.service'; import { CookieAcceptanceComponent } from './shared/cookie-acceptance-popup/cookie-acceptance.component'; +import { RouterLink } from '@angular/router'; @Component({ selector: 'st-cookies', standalone: true, imports: [ - CommonModule + CommonModule, + RouterLink ], templateUrl: './cookies.component.html', styleUrl: './cookies.component.scss', diff --git a/src/app/pages/cookies/shared/cookie-acceptance-popup/cookie-acceptance.component.ts b/src/app/pages/cookies/shared/cookie-acceptance-popup/cookie-acceptance.component.ts index 69847c9..1d4b663 100644 --- a/src/app/pages/cookies/shared/cookie-acceptance-popup/cookie-acceptance.component.ts +++ b/src/app/pages/cookies/shared/cookie-acceptance-popup/cookie-acceptance.component.ts @@ -68,6 +68,7 @@ export class CookieAcceptanceComponent { * Called when a user rejects our policies. */ onRejectPolicies() { + this.safeStorageService.clear(); this.safeStorageService.save(SafeStorageService.STORAGE_ALLOWED_KEY, false); } } From 667517edec843b1b0ede90b4b3c9fc7d4d6fb858 Mon Sep 17 00:00:00 2001 From: Scott Straughan Date: Thu, 26 Sep 2024 19:26:42 +0100 Subject: [PATCH 09/27] Removed automatic last-visit saving on changed page, it's not reaquired. --- src/app/pages/changed/changed.component.ts | 63 +++++----------------- 1 file changed, 12 insertions(+), 51 deletions(-) diff --git a/src/app/pages/changed/changed.component.ts b/src/app/pages/changed/changed.component.ts index 4d60cf1..680df3b 100644 --- a/src/app/pages/changed/changed.component.ts +++ b/src/app/pages/changed/changed.component.ts @@ -20,7 +20,6 @@ import { ChangeDetectionStrategy, Component, computed, - OnDestroy, OnInit, Signal, signal, @@ -57,16 +56,16 @@ import { ChangeStartDateComponent } from './change-start-date-popup/change-start import { catchError, firstValueFrom, of, tap } from 'rxjs'; import { LoadingState } from '../../shared/LoadingState'; import { LoadingComponent } from '../../shared/components/loading/loading.component'; -import { SafeStorageService } from '../../shared/services/safe-storage.service'; import { PlatformService } from '../../shared/services/platform.service'; +import { ChangedService } from '../../shared/services/changed.service'; @Component({ selector: 'st-changed', standalone: true, imports: [ CommonModule, - NewsWidgetComponent, RouterLink, + NewsWidgetComponent, ProjectWidgetComponent, VideoWidgetComponent, ResearchWidgetComponent, @@ -77,9 +76,7 @@ import { PlatformService } from '../../shared/services/platform.service'; styleUrl: './changed.component.scss', changeDetection: ChangeDetectionStrategy.OnPush }) -export class ChangedComponent implements OnInit, OnDestroy { - protected static readonly STORAGE_LAST_VISIT = 'st-last-visit-date'; - +export class ChangedComponent implements OnInit { protected readonly startDate: WritableSignal = signal(undefined); protected readonly currentDate: WritableSignal = signal(new Date()); protected readonly quickLinks: Signal; @@ -99,13 +96,11 @@ export class ChangedComponent implements OnInit, OnDestroy { protected readonly ResearchWidgetLayout = ResearchWidgetLayout; protected readonly LoadingState = LoadingState; - protected saveTimer: any = undefined; - /** * Constructor. * @param title + * @param changedService * @param platformService - * @param safeStorageService * @param popupService * @param projectService * @param newsService @@ -114,8 +109,8 @@ export class ChangedComponent implements OnInit, OnDestroy { */ constructor( protected title: Title, + protected changedService: ChangedService, protected platformService: PlatformService, - protected safeStorageService: SafeStorageService, protected popupService: PopupService, protected projectService: ProjectService, protected newsService: NewsService, @@ -161,34 +156,14 @@ export class ChangedComponent implements OnInit, OnDestroy { */ ngOnInit() { if (this.platformService.isClient()) { - if (this.safeStorageService.has(ChangedComponent.STORAGE_LAST_VISIT)) { - const lastVisitDate = new Date(this.safeStorageService.get(ChangedComponent.STORAGE_LAST_VISIT)); + const lastVisitDate = this.changedService.lastVisitDate(); + + if (lastVisitDate) { this.startDate.set(lastVisitDate); this.reload(); } else { this.onDateSelectorClicked(); - this.saveLastVisit(); } - - this.saveTimer = setInterval(() => { - this.saveLastVisit(); - }, 6000); - } - } - - /** - * @inheritdoc - */ - ngOnDestroy() { - this.stopAutosave(); - } - - /** - * Stop any autosave timers. - */ - stopAutosave() { - if (this.saveTimer) { - clearTimeout(this.saveTimer); } } @@ -199,6 +174,7 @@ export class ChangedComponent implements OnInit, OnDestroy { const startDate = this.startDate(); if (!startDate) { + // Escape early if no start date is set return; } @@ -213,6 +189,9 @@ export class ChangedComponent implements OnInit, OnDestroy { ChangedComponent.populate( this.videosService, this.videoLoadingState, this.updatedVideos, startDate); + + // Update the last visit time + this.changedService.saveLastVisitDate(); } /** @@ -223,31 +202,13 @@ export class ChangedComponent implements OnInit, OnDestroy { .then( (date) => { if (date) { - this.stopAutosave(); this.startDate.set(date); this.reload(); - this.saveLastVisit(date); } } ); } - /** - * Update the last visit date in storage. - */ - saveLastVisit(date?: Date) { - try { - if (!date) { - date = new Date(); - } - - this.safeStorageService.save(ChangedComponent.STORAGE_LAST_VISIT, date); - console.log('Successfully updated last visit time.'); - } catch (e) { - console.error('Skipping saving last visit time due to error.'); - } - } - /** * Populate results from the provided services, updating loading states etc. * @param service From 8ce46576fdff89a23cf0de1f84e9894edc69863a Mon Sep 17 00:00:00 2001 From: Scott Straughan Date: Thu, 26 Sep 2024 19:27:07 +0100 Subject: [PATCH 10/27] Added standalone changed.service.ts to share code across site. --- src/app/shared/services/changed.service.ts | 70 ++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 src/app/shared/services/changed.service.ts diff --git a/src/app/shared/services/changed.service.ts b/src/app/shared/services/changed.service.ts new file mode 100644 index 0000000..8d1becc --- /dev/null +++ b/src/app/shared/services/changed.service.ts @@ -0,0 +1,70 @@ +/*--------------------------------------------------------------------------------------------- + * + * Copyright (C) Codeplay Software Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + *--------------------------------------------------------------------------------------------*/ + +import { Injectable } from '@angular/core'; +import { SafeStorageService } from './safe-storage.service'; + +@Injectable({ + providedIn: 'root' +}) +export class ChangedService { + /** + * The storage key to use for tracking the users last access date. + * @protected + */ + protected static readonly STORAGE_LAST_VISIT = 'st-last-visit-date'; + + /** + * Constructor. + * @param safeStorageService + */ + constructor( + protected safeStorageService: SafeStorageService + ) { } + + /** + * Determine if there are changes available since the last visit. + */ + changesAvailable(): boolean { + return true; + } + + /** + * Get the date of the users last known visit. Will return undefined if no date is saved. + */ + lastVisitDate(): Date | undefined { + if (this.safeStorageService.has(ChangedService.STORAGE_LAST_VISIT)) { + return new Date(this.safeStorageService.get(ChangedService.STORAGE_LAST_VISIT)); + } + + return undefined; + } + + /** + * Save the last visit date to the local storage service. + * @param date + */ + saveLastVisitDate(date?: Date) { + try { + date = date ?? new Date(); + this.safeStorageService.save(ChangedService.STORAGE_LAST_VISIT, date); + } catch (e) { + // Do nothing + } + } +} From 9c1b49113430f0a68ea03afa2d99e9159ba242bf Mon Sep 17 00:00:00 2001 From: Scott Straughan Date: Thu, 26 Sep 2024 19:27:24 +0100 Subject: [PATCH 11/27] Added missing cookies to allowed cookie key list. --- src/environments/environment.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/environments/environment.ts b/src/environments/environment.ts index 7358879..6ce8680 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -34,6 +34,7 @@ export const environment = { 'st-enable-tracking', 'st-playground-compiler-tag', 'st-playground-saved', - 'st-last-visit-date' + 'st-last-visit-date', + 'st-blocked-alerts', ] }; From 1ea6237e0168e942ac789973c91c79b2e48d2991 Mon Sep 17 00:00:00 2001 From: Scott Straughan Date: Thu, 26 Sep 2024 19:28:14 +0100 Subject: [PATCH 12/27] Added home page alert. --- src/app/app.component.scss | 1 + src/app/app.config.ts | 2 + src/app/pages/home/home.component.html | 7 + src/app/pages/home/home.component.scss | 16 ++ src/app/pages/home/home.component.ts | 26 +++ .../site-wide-alert/alerts.component.html | 21 +++ .../site-wide-alert/alerts.component.scss | 111 ++++++++++++ .../site-wide-alert/alerts.component.ts | 113 ++++++++++++ src/app/shared/services/alert.service.ts | 164 ++++++++++++++++++ 9 files changed, 461 insertions(+) create mode 100644 src/app/shared/components/site-wide-alert/alerts.component.html create mode 100644 src/app/shared/components/site-wide-alert/alerts.component.scss create mode 100644 src/app/shared/components/site-wide-alert/alerts.component.ts create mode 100644 src/app/shared/services/alert.service.ts diff --git a/src/app/app.component.scss b/src/app/app.component.scss index dbbf829..b0fcb7f 100644 --- a/src/app/app.component.scss +++ b/src/app/app.component.scss @@ -4,6 +4,7 @@ nav { box-shadow: 1px 1px 5px 0 rgba(0, 0, 0, .1); padding: 0 2rem; position: relative; + z-index: 999; input { display: none; diff --git a/src/app/app.config.ts b/src/app/app.config.ts index 30f3228..c8869e6 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -32,6 +32,7 @@ import { TitleCasePipe } from '@angular/common'; import { httpCacheInterceptor } from './http-cache.interceptor'; import { appLegacyRoutes } from './app.legacy-routes'; import { provideMarkdown } from 'ngx-markdown'; +import { provideAnimations } from '@angular/platform-browser/animations'; const scrollConfig: InMemoryScrollingOptions = { scrollPositionRestoration: 'top', @@ -88,5 +89,6 @@ export const appConfig: ApplicationConfig = { ), TitleCasePipe, provideMarkdown(), + provideAnimations() ] }; diff --git a/src/app/pages/home/home.component.html b/src/app/pages/home/home.component.html index 31cb5d1..afd01b7 100644 --- a/src/app/pages/home/home.component.html +++ b/src/app/pages/home/home.component.html @@ -1,3 +1,10 @@ + +
+
+ +
+
+
diff --git a/src/app/pages/home/home.component.scss b/src/app/pages/home/home.component.scss index 8cd2f24..f22181f 100644 --- a/src/app/pages/home/home.component.scss +++ b/src/app/pages/home/home.component.scss @@ -1,3 +1,19 @@ +/** + * Section: Alerts + */ +section#alerts { + padding: 0; + + @media screen and (min-width: 1000px) { + position: relative; + height: 0; + left: 0; + top: 0; + width: 100%; + z-index: 99; + } +} + /** * Section: Intro */ diff --git a/src/app/pages/home/home.component.ts b/src/app/pages/home/home.component.ts index 2a6b861..ea22b01 100644 --- a/src/app/pages/home/home.component.ts +++ b/src/app/pages/home/home.component.ts @@ -55,6 +55,8 @@ import { ImplementationActivityService } from '../../shared/services/models/impl import { toSignal } from '@angular/core/rxjs-interop'; import { CalenderWidgetComponent } from '../calendar/shared/calendar-item-widget/calender-widget.component'; import { environment } from '../../../environments/environment'; +import { AlertsComponent } from '../../shared/components/site-wide-alert/alerts.component'; +import { AlertService } from '../../shared/services/alert.service'; @Component({ selector: 'st-home', @@ -72,6 +74,7 @@ import { environment } from '../../../environments/environment'; ScrollingPanelComponent, CalenderWidgetComponent, NgOptimizedImage, + AlertsComponent, ], templateUrl: './home.component.html', styleUrls: [ @@ -101,6 +104,7 @@ export class HomeComponent implements SearchablePage { * Constructor * @param meta * @param titleService + * @param alertService * @param playgroundSampleService * @param newsService * @param communityUpdateService @@ -113,6 +117,7 @@ export class HomeComponent implements SearchablePage { constructor( protected meta: Meta, protected titleService: Title, + protected alertService: AlertService, protected playgroundSampleService: PlaygroundSampleService, protected newsService: NewsService, protected communityUpdateService: ImplementationActivityService, @@ -162,6 +167,27 @@ export class HomeComponent implements SearchablePage { this.researchCount = toSignal( this.researchService.count(), { initialValue: 0 }); + + this.alertService.add( + 'whats-changed', + 'Just want to see what has changed?', + 'We have had 10 new updates. Click to view all the new projects, news, research papers and videos since your last visit.', + 'newspaper', + '/changed'); + + this.alertService.add( + 'whats-changed-1', + '123123', + 'We have had 10 new updates. Click to view all the new projects, news, research papers and videos since your last visit.', + 'newspaper', + '/changed'); + + this.alertService.add( + 'whats-changed-2', + 'sfdgsdfgsd', + 'We have had 10 new updates. Click to view all the new projects, news, research papers and videos since your last visit.', + 'newspaper', + '/changed'); } /** diff --git a/src/app/shared/components/site-wide-alert/alerts.component.html b/src/app/shared/components/site-wide-alert/alerts.component.html new file mode 100644 index 0000000..9f5cc1a --- /dev/null +++ b/src/app/shared/components/site-wide-alert/alerts.component.html @@ -0,0 +1,21 @@ +@if (alert(); as alert) { + +} diff --git a/src/app/shared/components/site-wide-alert/alerts.component.scss b/src/app/shared/components/site-wide-alert/alerts.component.scss new file mode 100644 index 0000000..0435c6e --- /dev/null +++ b/src/app/shared/components/site-wide-alert/alerts.component.scss @@ -0,0 +1,111 @@ +:host { + $overflow-width: 20px; + $min-height: 65px; + $hover-height: 70px; + $side-padding: 1.5rem; + + display: block; + position: relative; + width: calc(100% + $overflow-width); + left: calc($overflow-width / 2 * -1); + color: var(--color-white); + + * { + padding: 0; + margin: 0; + } + + .container { + display: flex; + position: relative; + background: linear-gradient(90deg, var(--color-orange) 61%, rgba(35, 10, 46, 1) 100%); + border-radius: 0 0 var(--border-radius) var(--border-radius); + box-shadow: 0 3px 10px 0 rgba(0, 0, 0, .15); + min-height: $min-height; + transition: var(--transition-fast); + overflow: hidden; + + .background { + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + transition: var(--transition-fast); + background: linear-gradient(90deg, var(--color-orange) 20%, rgba(35, 10, 46, 1) 100%); + opacity: 0; + } + + &:hover { + min-height: $hover-height; + box-shadow: 0 3px 10px 0 rgba(0, 0, 0, .20); + + .background { + opacity: 1; + } + } + + .content, + .buttons { + position: relative; + } + + .content { + flex: 1; + display: flex; + gap: 1rem; + align-items: center; + cursor: pointer; + padding: 0 0 0 $side-padding; + + .icon { + height: 100%; + display: flex; + justify-content: center; + align-items: center; + + span { + font-size: 30px; + } + } + + .messages { + flex: 1; + + h1 { + font-size: 1rem; + } + + h2 { + font-size: .7rem; + font-weight: normal; + opacity: .6; + } + } + } + + .buttons { + display: flex; + gap: .5rem; + padding: .5rem $side-padding; + + a.button { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + padding: 0 1rem; + background-color: rgba(255, 255, 255, .2); + opacity: .5; + + &:hover { + opacity: 1; + } + + span { + margin-right: .4rem; + } + } + } + } +} diff --git a/src/app/shared/components/site-wide-alert/alerts.component.ts b/src/app/shared/components/site-wide-alert/alerts.component.ts new file mode 100644 index 0000000..357433c --- /dev/null +++ b/src/app/shared/components/site-wide-alert/alerts.component.ts @@ -0,0 +1,113 @@ +/*--------------------------------------------------------------------------------------------- + * + * Copyright (C) Codeplay Software Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + *--------------------------------------------------------------------------------------------*/ + +import { + ChangeDetectionStrategy, + Component, + OnInit, + signal, + WritableSignal +} from '@angular/core'; +import { Alert, AlertService } from '../../services/alert.service'; +import { RouterLink } from '@angular/router'; +import { tap } from 'rxjs'; +import { PlatformService } from '../../services/platform.service'; +import { SafeStorageService } from '../../services/safe-storage.service'; +import { animate, style, transition, trigger } from '@angular/animations'; +import { CommonModule } from '@angular/common'; + +@Component({ + selector: 'st-alerts', + standalone: true, + templateUrl: './alerts.component.html', + imports: [ + RouterLink, + CommonModule, + ], + styleUrl: './alerts.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, + animations: [ + trigger('slideInTop', [ + transition(':enter', [ + style({ transform: 'translateY(-100%)' }), + animate('500ms ease-out', style({ transform: 'translateY(0)' })), + ]), + transition(':leave', [ + animate('500ms ease-out', style({ transform: 'translateY(-100%)' })), + ]) + ]) +], +}) +export class AlertsComponent implements OnInit { + /** + * The signal to store the currently visible alert for rendering via the template. + * @protected + */ + protected alert: WritableSignal = signal(undefined); + + /** + * Constructor. + * @param platformService + * @param safeStorageService + * @param alertService + */ + constructor( + protected platformService: PlatformService, + protected safeStorageService: SafeStorageService, + protected alertService: AlertService + ) { } + + /** + * @inheritdoc + */ + ngOnInit() { + if (!this.platformService.isClient()) { + return; + } + + // If we are not allowed to store cookies allowing users to hide/block alerts, we shouldn't really show any of them + // otherwise they will just annoy the user. In this case, just return and do nothing. + if (!this.safeStorageService.allowed()) { + return ; + } + + this.alertService.observe() + .pipe( + tap((alert) => { + this.alert.set(alert); + }) + ) + .subscribe(); + } + + /** + * Called when a user chooses to block/hide an alert. + * @param alert + */ + onBlockAlert(alert: Alert) { + this.alertService.block(alert) + } + + /** + * Called when a user clicks on an alert. + * @param alert + */ + onAlertClicked(alert: Alert) { + this.onBlockAlert(alert); + } +} diff --git a/src/app/shared/services/alert.service.ts b/src/app/shared/services/alert.service.ts new file mode 100644 index 0000000..d8eeac1 --- /dev/null +++ b/src/app/shared/services/alert.service.ts @@ -0,0 +1,164 @@ +/*--------------------------------------------------------------------------------------------- + * + * Copyright (C) Codeplay Software Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + *--------------------------------------------------------------------------------------------*/ + +import { Inject, Injectable, PLATFORM_ID } from '@angular/core'; +import { isPlatformServer } from '@angular/common'; +import { BehaviorSubject, Observable } from 'rxjs'; +import { SafeStorageService } from './safe-storage.service'; + +@Injectable({ + providedIn: 'root' +}) +export class AlertService { + /** + * An array of alerts to show to the user. + * @protected + */ + protected alerts: Alert[] = []; + + /** + * Subject, used to notify observers of changes to the alerts. + * @protected + */ + protected behaviorSubject: BehaviorSubject = new BehaviorSubject(undefined); + + /** + * Constructor. + * @param safeStorageService + */ + constructor( + protected safeStorageService: SafeStorageService + ) { } + + /** + * Get an observable that will notify of any alert changes. + */ + observe(): Observable { + return this.behaviorSubject; + } + + /** + * Add a new alert. + * @param id + * @param message + * @param subMessage + * @param icon + * @param href + */ + add( + id: string, + message: string, + subMessage?: string, + icon?: string, + href?: string + ) { + // If the alert id is in the blocked list, exit early + if (this.isAlertBlocked(id)) { + return ; + } + + this.alerts.push({ + id: id, + icon: icon ?? 'notifications', + message: message, + subMessage: subMessage, + href: href + }); + + this.notify(); + } + + /** + * Block an alert. + * @param alert + */ + block( + alert: Alert + ) { + // Remove the alert from the internal alert list by searching for it's id + for (const alertIndex in this.alerts) { + const currentAlert = this.alerts[alertIndex]; + + if (currentAlert.id == alert.id) { + this.alerts.splice(Number.parseInt(alertIndex), 1); + } + } + + let blockedAlerts = []; + if (this.safeStorageService.has('st-blocked-alerts')) { + blockedAlerts = this.safeStorageService.get('st-blocked-alerts'); + } + + blockedAlerts.push(alert.id); + this.safeStorageService.save('st-blocked-alerts', blockedAlerts); + + this.notify(); + } + + /** + * Check if the alert id is within the block list. + * @param alertId + */ + isAlertBlocked( + alertId: string + ): boolean { + if (this.safeStorageService.has('st-blocked-alerts')) { + return this.safeStorageService.get('st-blocked-alerts').includes(alertId); + } + + return false; + } + + /** + * Notify any subscribed users that there are new alerts. + */ + notify() { + this.behaviorSubject.next(this.getNextAlert()); + } + + /** + * Check if there are alerts available. + */ + has(): boolean { + return this.alerts.length > 0; + } + + /** + * Gets the next alert in the alert list. + */ + getNextAlert(): Alert | undefined { + const alerts = this.alerts.slice(); + + if (alerts.length == 0) { + return undefined; + } + + return alerts[0]; + } +} + +/** + * Represents an alert. + */ +export interface Alert { + id: string + icon: string + message: string + subMessage?: string + href?: string +} From 5b5c94090763e39166b84d04203c8a1ebf07599d Mon Sep 17 00:00:00 2001 From: Scott Straughan Date: Fri, 27 Sep 2024 12:48:34 +0100 Subject: [PATCH 13/27] Added missing storage keys. --- src/environments/environment.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/environments/environment.ts b/src/environments/environment.ts index 6ce8680..f9b7917 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -28,6 +28,7 @@ export const environment = { }, privacy_policy_email: 'info@codeplay.com', fathom_analytics_token: 'MMWGQHXZ', + allowed_storage_keys: [ 'st-cookies-accepted', 'st-dark-mode-enabled', @@ -36,5 +37,6 @@ export const environment = { 'st-playground-saved', 'st-last-visit-date', 'st-blocked-alerts', - ] + 'st-enable-alerts', + ], }; From 233491a26d9c51183898a09bb6ac3e2eb5e7b38f Mon Sep 17 00:00:00 2001 From: Scott Straughan Date: Fri, 27 Sep 2024 12:48:51 +0100 Subject: [PATCH 14/27] Added alerts service to main page to allow all pages to set alerts. --- src/app/app.component.html | 2 ++ src/app/app.component.ts | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/app/app.component.html b/src/app/app.component.html index 97faf63..2259c81 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -145,6 +145,7 @@
+
@@ -173,6 +174,7 @@

Network

Useful Links

    +
  • What's Changed?
  • Cookie Policy
  • Privacy Policy
  • Settings
  • diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 2a6d561..315d261 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -38,6 +38,7 @@ import { CookieAcceptanceComponent } from './pages/cookies/shared/cookie-accepta import { toSignal } from '@angular/core/rxjs-interop'; import { PlatformService } from './shared/services/platform.service'; import { SafeStorageService } from './shared/services/safe-storage.service'; +import { AlertsComponent } from './shared/components/site-wide-alert/alerts.component'; @Component({ selector: 'app-root', @@ -50,7 +51,8 @@ import { SafeStorageService } from './shared/services/safe-storage.service'; NgOptimizedImage, SearchComponent, CookieAcceptanceComponent, - NgClass + NgClass, + AlertsComponent ], templateUrl: './app.component.html', styleUrl: './app.component.scss', From 6df1e83d611fc695bb356e6c83e522ae4729edc3 Mon Sep 17 00:00:00 2001 From: Scott Straughan Date: Fri, 27 Sep 2024 12:49:21 +0100 Subject: [PATCH 15/27] Added default enabled alerts when the storage service state is enabled/disabled. --- .../shared/services/safe-storage.service.ts | 52 +++++++++++++------ 1 file changed, 37 insertions(+), 15 deletions(-) diff --git a/src/app/shared/services/safe-storage.service.ts b/src/app/shared/services/safe-storage.service.ts index 6760397..efc481b 100644 --- a/src/app/shared/services/safe-storage.service.ts +++ b/src/app/shared/services/safe-storage.service.ts @@ -29,6 +29,11 @@ export class SafeStorageService { private readonly subject: BehaviorSubject; private readonly allowedStorageKeys: string[] = environment.allowed_storage_keys; + protected readonly enableByDefault = [ + 'st-enable-tracking', + 'st-enable-alerts' + ] + /** * Constructor. * @param storageService @@ -62,9 +67,22 @@ export class SafeStorageService { this.storageService.remove(key); } else { this.storageService.clear(); - this.storageService.set(SafeStorageService.STORAGE_ALLOWED_KEY, this.allowed()); + } + } + + /** + * Set if we are allowed or not to store data to the storage service. + */ + setStorageAllowed(enable: boolean) { + if (enable) { + for (const key of this.enableByDefault) { + this.storageService.set(key, enable); + } + } else { + this.clear(); } + this.storageService.set(SafeStorageService.STORAGE_ALLOWED_KEY, enable); this.notify(); } @@ -72,29 +90,33 @@ export class SafeStorageService { * Save a value to the safe storage service. * @param key the key to use to save and access the value * @param value the value to store - * @param notify if we wish to notify subscribers that the state has changed * @throws DefaultStorageKeys will be thrown if we are not allowed to store data */ save( key: string, - value: any, - notify: boolean = true + value: any ) { - if (key != SafeStorageService.STORAGE_ALLOWED_KEY) { - if (!this.allowed()) { - throw new StorageNotEnabledError(); - } + // If the key is the STORAGE_ALLOWED_KEY, handle this separately + if (key == SafeStorageService.STORAGE_ALLOWED_KEY) { + this.setStorageAllowed(value); + return ; + } - if (!this.allowedStorageKeys.includes(key)) { - throw new KeyNotAllowedError(`The key "${key}" is not in the allowed key list.`); - } + // Check if we are allowed to store to the storage service. We would be denied if the user has not allowed + // cookies/storage. + if (!this.allowed()) { + throw new StorageNotEnabledError('The storage service is not enabled.'); + } + + // Check if the key is in the allowed list of keys, this prevents us storing something we haven't declared + if (!this.allowedStorageKeys.includes(key)) { + throw new KeyNotAllowedError(`The key "${key}" is not in the allowed key list.`); } this.storageService.set(key, value); - if (notify) { - this.notify(); - } + // Notify any observers of change to the storage state + this.notify(); } /** @@ -120,7 +142,7 @@ export class SafeStorageService { /** * Get the current state of all known allowed keys. */ - state() { + private state() { const state: any = {}; for (const key of this.allowedStorageKeys) { From 2c3505e80de6d684578d1ce3cdbc960223062443 Mon Sep 17 00:00:00 2001 From: Scott Straughan Date: Fri, 27 Sep 2024 12:49:29 +0100 Subject: [PATCH 16/27] Removed unused code. --- src/app/shared/services/changed.service.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/app/shared/services/changed.service.ts b/src/app/shared/services/changed.service.ts index 8d1becc..0d6f4bd 100644 --- a/src/app/shared/services/changed.service.ts +++ b/src/app/shared/services/changed.service.ts @@ -37,13 +37,6 @@ export class ChangedService { protected safeStorageService: SafeStorageService ) { } - /** - * Determine if there are changes available since the last visit. - */ - changesAvailable(): boolean { - return true; - } - /** * Get the date of the users last known visit. Will return undefined if no date is saved. */ From 77ba4b802ef016cc78e230bf2cc0e429bf8a017b Mon Sep 17 00:00:00 2001 From: Scott Straughan Date: Fri, 27 Sep 2024 12:49:38 +0100 Subject: [PATCH 17/27] Revamped how the alert service works. --- src/app/shared/services/alert.service.ts | 141 +++++++++++++++-------- 1 file changed, 95 insertions(+), 46 deletions(-) diff --git a/src/app/shared/services/alert.service.ts b/src/app/shared/services/alert.service.ts index d8eeac1..b3fbd21 100644 --- a/src/app/shared/services/alert.service.ts +++ b/src/app/shared/services/alert.service.ts @@ -16,15 +16,31 @@ * *--------------------------------------------------------------------------------------------*/ -import { Inject, Injectable, PLATFORM_ID } from '@angular/core'; -import { isPlatformServer } from '@angular/common'; -import { BehaviorSubject, Observable } from 'rxjs'; +import { Injectable } from '@angular/core'; +import { BehaviorSubject, Observable, tap } from 'rxjs'; import { SafeStorageService } from './safe-storage.service'; +import { PlatformService } from './platform.service'; @Injectable({ providedIn: 'root' }) export class AlertService { + /** + * Storage key for the cookie service. + */ + public static readonly ENABLE_ALERTS_STORAGE_KEY = 'st-enable-alerts'; + + /** + * Default timeout for an alert. + */ + public static readonly DEFAULT_TIMEOUT = 4000; + + /** + * If the service is enabled or not. + * @protected + */ + protected enabled: boolean = false; + /** * An array of alerts to show to the user. * @protected @@ -35,52 +51,66 @@ export class AlertService { * Subject, used to notify observers of changes to the alerts. * @protected */ - protected behaviorSubject: BehaviorSubject = new BehaviorSubject(undefined); + protected behaviorSubject: BehaviorSubject = new BehaviorSubject([]); /** * Constructor. * @param safeStorageService + * @param platformService */ constructor( - protected safeStorageService: SafeStorageService - ) { } + protected safeStorageService: SafeStorageService, + protected platformService: PlatformService, + ) { + this.safeStorageService.observe() + .pipe( + tap((state) => { + this.enabled = state[AlertService.ENABLE_ALERTS_STORAGE_KEY] ?? false; + + if (this.enabled) { + this.notify(); + } + }) + ) + .subscribe(); + } /** * Get an observable that will notify of any alert changes. */ - observe(): Observable { + observe(): Observable { return this.behaviorSubject; } /** * Add a new alert. - * @param id - * @param message - * @param subMessage - * @param icon - * @param href + * @param alert */ add( - id: string, - message: string, - subMessage?: string, - icon?: string, - href?: string + alert: Alert ) { - // If the alert id is in the blocked list, exit early - if (this.isAlertBlocked(id)) { + // Don't do anything if we are not a client + if (!this.platformService.isClient()) { return ; } - this.alerts.push({ - id: id, - icon: icon ?? 'notifications', - message: message, - subMessage: subMessage, - href: href - }); + // If the service is disabled or if the alert is blocked, don't show it + if (this.isAlertBlocked(alert.id)) { + console.error('Not showing alert, service is disabled or alert is blocked.'); + return ; + } + + this.alerts.push(alert); this.notify(); + + if (alert.persistent) { + return ; + } + + setTimeout(() => { + this.delete(alert); + }, AlertService.DEFAULT_TIMEOUT); } /** @@ -90,15 +120,6 @@ export class AlertService { block( alert: Alert ) { - // Remove the alert from the internal alert list by searching for it's id - for (const alertIndex in this.alerts) { - const currentAlert = this.alerts[alertIndex]; - - if (currentAlert.id == alert.id) { - this.alerts.splice(Number.parseInt(alertIndex), 1); - } - } - let blockedAlerts = []; if (this.safeStorageService.has('st-blocked-alerts')) { blockedAlerts = this.safeStorageService.get('st-blocked-alerts'); @@ -107,6 +128,35 @@ export class AlertService { blockedAlerts.push(alert.id); this.safeStorageService.save('st-blocked-alerts', blockedAlerts); + this.delete(alert); + } + + /** + * Delete an alert. + * @param alert + */ + delete( + alert: Alert + ) { + this.deleteById(alert.id); + } + + /** + * Delete an alert by its id. + * @param alertId + */ + deleteById( + alertId: string + ) { + // Remove the alert from the internal alert list by searching for it's id + for (const alertIndex in this.alerts) { + const currentAlert = this.alerts[alertIndex]; + + if (currentAlert.id == alertId) { + this.alerts.splice(Number.parseInt(alertIndex), 1); + } + } + this.notify(); } @@ -128,7 +178,11 @@ export class AlertService { * Notify any subscribed users that there are new alerts. */ notify() { - this.behaviorSubject.next(this.getNextAlert()); + if (!this.enabled) { + return ; + } + + this.behaviorSubject.next(this.getAlerts()); } /** @@ -141,14 +195,8 @@ export class AlertService { /** * Gets the next alert in the alert list. */ - getNextAlert(): Alert | undefined { - const alerts = this.alerts.slice(); - - if (alerts.length == 0) { - return undefined; - } - - return alerts[0]; + getAlerts(): Alert[] { + return this.alerts.slice(); } } @@ -158,7 +206,8 @@ export class AlertService { export interface Alert { id: string icon: string - message: string - subMessage?: string + title: string + persistent?: boolean + description?: string href?: string } From 1b28992618d7095d64763b37b57ed9c3a561f679 Mon Sep 17 00:00:00 2001 From: Scott Straughan Date: Fri, 27 Sep 2024 12:50:03 +0100 Subject: [PATCH 18/27] Updated to a bottom, right alert based service that allows multiple alerts to be shown. --- .../site-wide-alert/alerts.component.html | 14 ++-- .../site-wide-alert/alerts.component.scss | 82 +++++++------------ .../site-wide-alert/alerts.component.ts | 20 ++--- 3 files changed, 44 insertions(+), 72 deletions(-) diff --git a/src/app/shared/components/site-wide-alert/alerts.component.html b/src/app/shared/components/site-wide-alert/alerts.component.html index 9f5cc1a..b25669a 100644 --- a/src/app/shared/components/site-wide-alert/alerts.component.html +++ b/src/app/shared/components/site-wide-alert/alerts.component.html @@ -1,21 +1,21 @@ -@if (alert(); as alert) { -
    -
    +@for (alert of alerts(); track alert.id) { + } diff --git a/src/app/shared/components/site-wide-alert/alerts.component.scss b/src/app/shared/components/site-wide-alert/alerts.component.scss index 0435c6e..376238a 100644 --- a/src/app/shared/components/site-wide-alert/alerts.component.scss +++ b/src/app/shared/components/site-wide-alert/alerts.component.scss @@ -1,62 +1,43 @@ :host { - $overflow-width: 20px; - $min-height: 65px; - $hover-height: 70px; - $side-padding: 1.5rem; + $side-padding: 1rem; + $container-padding: 1.5rem; - display: block; - position: relative; - width: calc(100% + $overflow-width); - left: calc($overflow-width / 2 * -1); - color: var(--color-white); + position: fixed; + display: flex; + flex-direction: column; + gap: 1rem; + bottom: $container-padding; + right: $container-padding; + z-index: 9999; * { - padding: 0; margin: 0; + padding: 0; } .container { + width: 430px; display: flex; - position: relative; - background: linear-gradient(90deg, var(--color-orange) 61%, rgba(35, 10, 46, 1) 100%); - border-radius: 0 0 var(--border-radius) var(--border-radius); - box-shadow: 0 3px 10px 0 rgba(0, 0, 0, .15); - min-height: $min-height; - transition: var(--transition-fast); overflow: hidden; - - .background { - position: absolute; - left: 0; - top: 0; - width: 100%; - height: 100%; - transition: var(--transition-fast); - background: linear-gradient(90deg, var(--color-orange) 20%, rgba(35, 10, 46, 1) 100%); - opacity: 0; - } + background-color: rgba(138, 40, 14, 0.6); + backdrop-filter: blur(15px); + -webkit-backdrop-filter: blur(15px); + border-radius: var(--border-radius); + box-shadow: 2px 2px 5px 0 rgba(0, 0, 0, .2); + transition: var(--transition-fast); + color: var(--color-white); &:hover { - min-height: $hover-height; - box-shadow: 0 3px 10px 0 rgba(0, 0, 0, .20); - - .background { - opacity: 1; - } - } - - .content, - .buttons { - position: relative; + box-shadow: 2px 2px 5px 0 rgba(0, 0, 0, .4); + background-color: var(--color-orange); } .content { - flex: 1; display: flex; + padding: $side-padding; gap: 1rem; - align-items: center; + flex: 1; cursor: pointer; - padding: 0 0 0 $side-padding; .icon { height: 100%; @@ -79,32 +60,29 @@ h2 { font-size: .7rem; font-weight: normal; - opacity: .6; + opacity: .8; + margin-top: .2rem; } } } .buttons { display: flex; - gap: .5rem; - padding: .5rem $side-padding; + gap: 1rem; - a.button { + a { display: flex; align-items: center; justify-content: center; - height: 100%; - padding: 0 1rem; - background-color: rgba(255, 255, 255, .2); + cursor: pointer; + padding: $side-padding; opacity: .5; + transition: var(--transition-fast); + background-color: rgba(0, 0, 0, .15); &:hover { opacity: 1; } - - span { - margin-right: .4rem; - } } } } diff --git a/src/app/shared/components/site-wide-alert/alerts.component.ts b/src/app/shared/components/site-wide-alert/alerts.component.ts index 357433c..2c50189 100644 --- a/src/app/shared/components/site-wide-alert/alerts.component.ts +++ b/src/app/shared/components/site-wide-alert/alerts.component.ts @@ -42,13 +42,13 @@ import { CommonModule } from '@angular/common'; styleUrl: './alerts.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, animations: [ - trigger('slideInTop', [ + trigger('slideInSide', [ transition(':enter', [ - style({ transform: 'translateY(-100%)' }), - animate('500ms ease-out', style({ transform: 'translateY(0)' })), + style({ transform: 'translateX(150%)', opacity: 0 }), + animate('500ms ease-out', style({ transform: 'translateX(0)', opacity: 1 })), ]), transition(':leave', [ - animate('500ms ease-out', style({ transform: 'translateY(-100%)' })), + animate('500ms ease-out', style({ transform: 'translateX(150%)', opacity: 0 })), ]) ]) ], @@ -58,7 +58,7 @@ export class AlertsComponent implements OnInit { * The signal to store the currently visible alert for rendering via the template. * @protected */ - protected alert: WritableSignal = signal(undefined); + protected alerts: WritableSignal = signal([]); /** * Constructor. @@ -80,16 +80,10 @@ export class AlertsComponent implements OnInit { return; } - // If we are not allowed to store cookies allowing users to hide/block alerts, we shouldn't really show any of them - // otherwise they will just annoy the user. In this case, just return and do nothing. - if (!this.safeStorageService.allowed()) { - return ; - } - this.alertService.observe() .pipe( - tap((alert) => { - this.alert.set(alert); + tap((alerts) => { + this.alerts.set(alerts); }) ) .subscribe(); From c3a896ba13c14ab84e1e8aa5b3a91bec48da56b9 Mon Sep 17 00:00:00 2001 From: Scott Straughan Date: Fri, 27 Sep 2024 12:50:29 +0100 Subject: [PATCH 19/27] Updated settings page to allow enabling/disabling of alerts. --- .../pages/settings/settings.component.html | 29 ++++++++++++++----- src/app/pages/settings/settings.component.ts | 20 +++++-------- 2 files changed, 28 insertions(+), 21 deletions(-) diff --git a/src/app/pages/settings/settings.component.html b/src/app/pages/settings/settings.component.html index e029e9f..40d370b 100644 --- a/src/app/pages/settings/settings.component.html +++ b/src/app/pages/settings/settings.component.html @@ -8,7 +8,7 @@

    We take your privacy seriously and we limit what we store.

- health_and_safety + settings
@@ -19,7 +19,6 @@

We take your privacy seriously and we limit what we store.

Change Your Settings

-

Enable Cookies/Storage

@@ -27,30 +26,44 @@

Enable Cookies/Storage

enabling dark mode.

- +
-

Enable Dark Mode

Enable or disable dark mode, site wide.

- +
-

Enable Tracking

Enable or disable anonymous tracking, we use this to improve the website.

- + +
+
+
+
+

Enable Alerts

+

Enable or disable alerts from showing up in your browser window.

+
+
+
-
\ No newline at end of file diff --git a/src/app/pages/settings/settings.component.ts b/src/app/pages/settings/settings.component.ts index c45d4d0..dca440c 100644 --- a/src/app/pages/settings/settings.component.ts +++ b/src/app/pages/settings/settings.component.ts @@ -35,9 +35,10 @@ import { SafeStorageService } from '../../shared/services/safe-storage.service'; changeDetection: ChangeDetectionStrategy.OnPush }) export class SettingsComponent { - enableStorage: WritableSignal = signal(false); - enableDarkMode: WritableSignal = signal(false); - enableTracking: WritableSignal = signal(false); + protected enableStorage: WritableSignal = signal(false); + protected enableDarkMode: WritableSignal = signal(false); + protected enableTracking: WritableSignal = signal(false); + protected enableAlerts: WritableSignal = signal(false); /** * Constructor. @@ -55,6 +56,7 @@ export class SettingsComponent { this.enableStorage.set(state['st-cookies-accepted'] == true); this.enableDarkMode.set(state['st-dark-mode-enabled'] == true); this.enableTracking.set(state['st-enable-tracking'] == true); + this.enableAlerts.set(state['st-enable-alerts'] == true); }) ).subscribe(); } @@ -62,15 +64,7 @@ export class SettingsComponent { /** * Called when a user changes any of the settings. */ - onStateChanged() { - if (!this.enableStorage()) { - // If storage is now disabled, clear any existing stored values - this.safeStorageService.clear(); - return ; - } - - this.safeStorageService.save('st-cookies-accepted', this.enableStorage(), false); - this.safeStorageService.save('st-dark-mode-enabled', this.enableDarkMode(), false); - this.safeStorageService.save('st-enable-tracking', this.enableTracking()); + onStateChanged(key: string) { + this.safeStorageService.save(key, this.enableStorage()); } } From 1a2c305b4947f7e9e86a891469d0adcd3f0de306 Mon Sep 17 00:00:00 2001 From: Scott Straughan Date: Fri, 27 Sep 2024 12:50:40 +0100 Subject: [PATCH 20/27] Added some alerts to home and playground. --- src/app/pages/home/home.component.html | 9 +--- src/app/pages/home/home.component.scss | 16 ------- src/app/pages/home/home.component.ts | 44 +++++++++---------- .../pages/playground/playground.component.ts | 19 +++++++- 4 files changed, 42 insertions(+), 46 deletions(-) diff --git a/src/app/pages/home/home.component.html b/src/app/pages/home/home.component.html index afd01b7..8aca804 100644 --- a/src/app/pages/home/home.component.html +++ b/src/app/pages/home/home.component.html @@ -1,10 +1,3 @@ - -
-
- -
-
-
@@ -52,6 +45,8 @@

The SYCL standard is defined by the Khronos Group, the open member-driven co

News & Updates

diff --git a/src/app/pages/home/home.component.scss b/src/app/pages/home/home.component.scss index f22181f..8cd2f24 100644 --- a/src/app/pages/home/home.component.scss +++ b/src/app/pages/home/home.component.scss @@ -1,19 +1,3 @@ -/** - * Section: Alerts - */ -section#alerts { - padding: 0; - - @media screen and (min-width: 1000px) { - position: relative; - height: 0; - left: 0; - top: 0; - width: 100%; - z-index: 99; - } -} - /** * Section: Intro */ diff --git a/src/app/pages/home/home.component.ts b/src/app/pages/home/home.component.ts index ea22b01..0b9c681 100644 --- a/src/app/pages/home/home.component.ts +++ b/src/app/pages/home/home.component.ts @@ -17,7 +17,7 @@ *--------------------------------------------------------------------------------------------*/ import { CommonModule, NgOptimizedImage } from '@angular/common'; -import { ChangeDetectionStrategy, Component, Signal } from '@angular/core'; +import { ChangeDetectionStrategy, Component, OnDestroy, OnInit, Signal } from '@angular/core'; import { RouterLink } from '@angular/router'; import { NewsModel } from '../../shared/models/news.model'; import { NewsService } from '../../shared/services/models/news.service'; @@ -83,7 +83,7 @@ import { AlertService } from '../../shared/services/alert.service'; ], changeDetection: ChangeDetectionStrategy.OnPush }) -export class HomeComponent implements SearchablePage { +export class HomeComponent implements SearchablePage, OnInit, OnDestroy { protected readonly news: Signal; protected readonly contributors: Signal; protected readonly communityUpdates: Signal; @@ -167,27 +167,27 @@ export class HomeComponent implements SearchablePage { this.researchCount = toSignal( this.researchService.count(), { initialValue: 0 }); + } - this.alertService.add( - 'whats-changed', - 'Just want to see what has changed?', - 'We have had 10 new updates. Click to view all the new projects, news, research papers and videos since your last visit.', - 'newspaper', - '/changed'); - - this.alertService.add( - 'whats-changed-1', - '123123', - 'We have had 10 new updates. Click to view all the new projects, news, research papers and videos since your last visit.', - 'newspaper', - '/changed'); - - this.alertService.add( - 'whats-changed-2', - 'sfdgsdfgsd', - 'We have had 10 new updates. Click to view all the new projects, news, research papers and videos since your last visit.', - 'newspaper', - '/changed'); + /** + * @inheritdoc + */ + ngOnInit() { + this.alertService.add({ + id: 'home-whats-changed', + icon: 'update', + title: 'Show me what has changed!', + description: 'Click here to see our new videos, news, projects and research papers, since your last visit.', + href: './changed', + persistent: true + }); + } + + /** + * @inheritdoc + */ + ngOnDestroy() { + this.alertService.deleteById('home-whats-changed'); } /** diff --git a/src/app/pages/playground/playground.component.ts b/src/app/pages/playground/playground.component.ts index 32eead4..6392a05 100644 --- a/src/app/pages/playground/playground.component.ts +++ b/src/app/pages/playground/playground.component.ts @@ -50,6 +50,7 @@ import { PlatformInfoPopupComponent } from './popups/platform-info/platform-info import { SharePopupComponent } from './popups/share/share-popup.component'; import { CompilerSelectorPopupComponent } from './popups/compiler-select/compiler-selector-popup.component'; import { SafeStorageService } from '../../shared/services/safe-storage.service'; +import { AlertService } from '../../shared/services/alert.service'; @Component({ selector: 'st-playground', @@ -104,6 +105,7 @@ export class PlaygroundComponent implements SearchablePage, OnInit, OnDestroy { * @param document * @param renderer * @param router + * @param alertService */ constructor( protected titleService: Title, @@ -115,7 +117,8 @@ export class PlaygroundComponent implements SearchablePage, OnInit, OnDestroy { protected activatedRoute: ActivatedRoute, @Inject(DOCUMENT) protected document: Document, protected renderer: Renderer2, - protected router: Router + protected router: Router, + protected alertService: AlertService ) { this.titleService.setTitle('Playground - SYCL.tech'); this.meta.addTag({ name: 'keywords', content: this.getKeywords().join(', ') }); @@ -240,8 +243,22 @@ export class PlaygroundComponent implements SearchablePage, OnInit, OnDestroy { if (compilationResult.isError()) { this.compileState.set(LoadingState.LOAD_FAILURE); this.showErrorPanel(); + + this.alertService.add({ + id: 'playground-compilation-result', + icon: 'thumb_down', + title: 'Compilation Failed', + description: `Your code has failed to compile, please check error window.` + }); } else { this.compileState.set(LoadingState.LOAD_SUCCESS); + + this.alertService.add({ + id: 'playground-compilation-result', + icon: 'thumb_up', + title: 'Compilation Successful', + description: `Your code was compiled successfully on ${compilationResult.platform}.` + }); } }) ); From 2a0bad51275168948a4c29d6285a1e630410bab0 Mon Sep 17 00:00:00 2001 From: Scott Straughan Date: Fri, 27 Sep 2024 12:51:01 +0100 Subject: [PATCH 21/27] Bug fixes to cookie page and popup. --- src/app/pages/cookies/cookies.component.html | 7 +++++++ .../cookie-acceptance-popup/cookie-acceptance.component.ts | 1 - 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/app/pages/cookies/cookies.component.html b/src/app/pages/cookies/cookies.component.html index 6bed8d5..11d7993 100644 --- a/src/app/pages/cookies/cookies.component.html +++ b/src/app/pages/cookies/cookies.component.html @@ -84,6 +84,13 @@

What We Store

1st Party Cookie
+ + st-enable-alerts + Used to allow or disallow alerts from showing in your browser window. + +
1st Party Cookie
+ + diff --git a/src/app/pages/cookies/shared/cookie-acceptance-popup/cookie-acceptance.component.ts b/src/app/pages/cookies/shared/cookie-acceptance-popup/cookie-acceptance.component.ts index 1d4b663..69847c9 100644 --- a/src/app/pages/cookies/shared/cookie-acceptance-popup/cookie-acceptance.component.ts +++ b/src/app/pages/cookies/shared/cookie-acceptance-popup/cookie-acceptance.component.ts @@ -68,7 +68,6 @@ export class CookieAcceptanceComponent { * Called when a user rejects our policies. */ onRejectPolicies() { - this.safeStorageService.clear(); this.safeStorageService.save(SafeStorageService.STORAGE_ALLOWED_KEY, false); } } From 6c9af63f011f15c40e7f4f9aec932a16473aa5d9 Mon Sep 17 00:00:00 2001 From: Scott Straughan Date: Fri, 27 Sep 2024 12:51:31 +0100 Subject: [PATCH 22/27] Updated changed.component.ts page to ensure a default date is set. --- src/app/pages/changed/changed.component.html | 2 +- src/app/pages/changed/changed.component.ts | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/app/pages/changed/changed.component.html b/src/app/pages/changed/changed.component.html index e3d4ebc..7fd89af 100644 --- a/src/app/pages/changed/changed.component.html +++ b/src/app/pages/changed/changed.component.html @@ -25,7 +25,7 @@

{{ currentDate() | date: 'mediumDate' }}

- newspaper + update
diff --git a/src/app/pages/changed/changed.component.ts b/src/app/pages/changed/changed.component.ts index 680df3b..a714008 100644 --- a/src/app/pages/changed/changed.component.ts +++ b/src/app/pages/changed/changed.component.ts @@ -156,12 +156,17 @@ export class ChangedComponent implements OnInit { */ ngOnInit() { if (this.platformService.isClient()) { - const lastVisitDate = this.changedService.lastVisitDate(); + let lastVisitDate = this.changedService.lastVisitDate(); if (lastVisitDate) { this.startDate.set(lastVisitDate); this.reload(); } else { + lastVisitDate = new Date(); + lastVisitDate.setMonth(lastVisitDate.getMonth() - 5); + lastVisitDate.setDate(1); + + this.startDate.set(lastVisitDate); this.onDateSelectorClicked(); } } From 7aa4ecd1f963f187671ca15ffca750425c871f5b Mon Sep 17 00:00:00 2001 From: Scott Straughan Date: Fri, 27 Sep 2024 13:00:26 +0100 Subject: [PATCH 23/27] Bug fixes to ensure subscriptions are properly closed or are self-closing. --- .../load-and-save-popup.component.ts | 24 ++++++++++--------- src/app/pages/settings/settings.component.ts | 22 +++++++++++++---- .../site-wide-alert/alerts.component.ts | 21 ++++++++++++---- 3 files changed, 48 insertions(+), 19 deletions(-) diff --git a/src/app/pages/playground/popups/load-and-save/load-and-save-popup.component.ts b/src/app/pages/playground/popups/load-and-save/load-and-save-popup.component.ts index a0ccfcc..a9f02a5 100644 --- a/src/app/pages/playground/popups/load-and-save/load-and-save-popup.component.ts +++ b/src/app/pages/playground/popups/load-and-save/load-and-save-popup.component.ts @@ -19,7 +19,7 @@ import { ChangeDetectionStrategy, Component, Inject, OnInit, signal, WritableSignal } from '@angular/core'; import { LoadingComponent } from '../../../../shared/components/loading/loading.component'; import { DatePipe } from '@angular/common'; -import { tap } from 'rxjs'; +import { take, tap } from 'rxjs'; import { PopupReference } from '../../../../shared/components/popup/popup.service'; import { RouterLink } from '@angular/router'; import { SafeStorageService } from '../../../../shared/services/safe-storage.service'; @@ -58,16 +58,18 @@ export class LoadAndSavePopupComponent implements OnInit { * @inheritDoc */ ngOnInit(): void { - this.safeStorageService.observe().pipe( - tap(() => { - this.storageEnabled.set(this.safeStorageService.allowed()); - - if (this.safeStorageService.has(LoadAndSavePopupComponent.storageKey)) { - const saved = this.safeStorageService.get(LoadAndSavePopupComponent.storageKey); - this.saved.set(saved.reverse()); - } - } - )).subscribe(); + this.safeStorageService.observe() + .pipe( + tap(() => { + this.storageEnabled.set(this.safeStorageService.allowed()); + + if (this.safeStorageService.has(LoadAndSavePopupComponent.storageKey)) { + const saved = this.safeStorageService.get(LoadAndSavePopupComponent.storageKey); + this.saved.set(saved.reverse()); + } + }), + take(1)) + .subscribe(); } /** diff --git a/src/app/pages/settings/settings.component.ts b/src/app/pages/settings/settings.component.ts index dca440c..4e40c96 100644 --- a/src/app/pages/settings/settings.component.ts +++ b/src/app/pages/settings/settings.component.ts @@ -16,10 +16,10 @@ * *--------------------------------------------------------------------------------------------*/ -import { ChangeDetectionStrategy, Component, signal, WritableSignal } from '@angular/core'; +import { ChangeDetectionStrategy, Component, OnDestroy, OnInit, signal, WritableSignal } from '@angular/core'; import { CommonModule } from '@angular/common'; import { SwitchComponent } from '../../shared/components/switch/switch.component'; -import { tap } from 'rxjs'; +import { Subscription, tap } from 'rxjs'; import { Title } from '@angular/platform-browser'; import { SafeStorageService } from '../../shared/services/safe-storage.service'; @@ -34,12 +34,14 @@ import { SafeStorageService } from '../../shared/services/safe-storage.service'; styleUrl: './settings.component.scss', changeDetection: ChangeDetectionStrategy.OnPush }) -export class SettingsComponent { +export class SettingsComponent implements OnInit, OnDestroy { protected enableStorage: WritableSignal = signal(false); protected enableDarkMode: WritableSignal = signal(false); protected enableTracking: WritableSignal = signal(false); protected enableAlerts: WritableSignal = signal(false); + protected storageSubscription?: Subscription; + /** * Constructor. * @param title @@ -50,8 +52,13 @@ export class SettingsComponent { protected safeStorageService: SafeStorageService, ) { this.title.setTitle('Settings - SYCL.tech'); + } - safeStorageService.observe().pipe( + /** + * @inheritdoc + */ + ngOnInit(): void { + this.storageSubscription = this.safeStorageService.observe().pipe( tap((state) => { this.enableStorage.set(state['st-cookies-accepted'] == true); this.enableDarkMode.set(state['st-dark-mode-enabled'] == true); @@ -61,6 +68,13 @@ export class SettingsComponent { ).subscribe(); } + /** + * @inheritdoc + */ + ngOnDestroy(): void { + this.storageSubscription?.unsubscribe(); + } + /** * Called when a user changes any of the settings. */ diff --git a/src/app/shared/components/site-wide-alert/alerts.component.ts b/src/app/shared/components/site-wide-alert/alerts.component.ts index 2c50189..5ee021e 100644 --- a/src/app/shared/components/site-wide-alert/alerts.component.ts +++ b/src/app/shared/components/site-wide-alert/alerts.component.ts @@ -18,14 +18,14 @@ import { ChangeDetectionStrategy, - Component, + Component, OnDestroy, OnInit, signal, WritableSignal } from '@angular/core'; import { Alert, AlertService } from '../../services/alert.service'; import { RouterLink } from '@angular/router'; -import { tap } from 'rxjs'; +import { Subscription, tap } from 'rxjs'; import { PlatformService } from '../../services/platform.service'; import { SafeStorageService } from '../../services/safe-storage.service'; import { animate, style, transition, trigger } from '@angular/animations'; @@ -53,13 +53,19 @@ import { CommonModule } from '@angular/common'; ]) ], }) -export class AlertsComponent implements OnInit { +export class AlertsComponent implements OnInit, OnDestroy { /** * The signal to store the currently visible alert for rendering via the template. * @protected */ protected alerts: WritableSignal = signal([]); + /** + * Subscription to track alerts. + * @protected + */ + protected alertSubscription?: Subscription; + /** * Constructor. * @param platformService @@ -80,7 +86,7 @@ export class AlertsComponent implements OnInit { return; } - this.alertService.observe() + this.alertSubscription = this.alertService.observe() .pipe( tap((alerts) => { this.alerts.set(alerts); @@ -89,6 +95,13 @@ export class AlertsComponent implements OnInit { .subscribe(); } + /** + * @inheritdoc + */ + ngOnDestroy() { + this.alertSubscription?.unsubscribe(); + } + /** * Called when a user chooses to block/hide an alert. * @param alert From 5110f0b229106394422441801eb8df9dc1bc8c17 Mon Sep 17 00:00:00 2001 From: Scott Straughan Date: Fri, 27 Sep 2024 13:01:53 +0100 Subject: [PATCH 24/27] Improved comments. --- src/app/pages/settings/settings.component.ts | 23 ++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/app/pages/settings/settings.component.ts b/src/app/pages/settings/settings.component.ts index 4e40c96..2813aa1 100644 --- a/src/app/pages/settings/settings.component.ts +++ b/src/app/pages/settings/settings.component.ts @@ -35,11 +35,34 @@ import { SafeStorageService } from '../../shared/services/safe-storage.service'; changeDetection: ChangeDetectionStrategy.OnPush }) export class SettingsComponent implements OnInit, OnDestroy { + /** + * Signal for the enable storage switch. + * @protected + */ protected enableStorage: WritableSignal = signal(false); + + /** + * Signal for the enable dark mode switch. + * @protected + */ protected enableDarkMode: WritableSignal = signal(false); + + /** + * Signal for the enable tracking switch. + * @protected + */ protected enableTracking: WritableSignal = signal(false); + + /** + * Signal for the enable alerts switch. + * @protected + */ protected enableAlerts: WritableSignal = signal(false); + /** + * Subscription to tracking storage changes. + * @protected + */ protected storageSubscription?: Subscription; /** From 01e111b340be9bb0c4ed095188ca549f905efb06 Mon Sep 17 00:00:00 2001 From: Scott Straughan Date: Fri, 27 Sep 2024 13:03:10 +0100 Subject: [PATCH 25/27] Variable name tweaks for consistency. --- src/app/app.component.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 315d261..baf2b04 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -86,10 +86,10 @@ export class AppComponent implements OnDestroy { protected enableDarkModeSwitch: WritableSignal = signal(false); /** - * State service subscription. + * Subscription used for storage changes. * @protected */ - protected state$: Subscription | undefined; + protected storageSubscription: Subscription | undefined; /** * Constructor. @@ -119,7 +119,7 @@ export class AppComponent implements OnDestroy { } }); - this.state$ = this.safeStorageService.observe().subscribe((state) => { + this.storageSubscription = this.safeStorageService.observe().subscribe((state) => { const fathomTrackers = this.document.documentElement.getElementsByClassName('fathom-tracking-script'); if (state['st-enable-tracking'] && fathomTrackers.length === 0) { @@ -158,7 +158,7 @@ export class AppComponent implements OnDestroy { * @inheritDoc */ ngOnDestroy() { - this.state$?.unsubscribe(); + this.storageSubscription?.unsubscribe(); } /** From cfaa72f059ef1d67050293f085e54b2f22d6190e Mon Sep 17 00:00:00 2001 From: Scott Straughan Date: Fri, 27 Sep 2024 13:25:43 +0100 Subject: [PATCH 26/27] Bug fix where settings were not being properly set. --- src/app/pages/settings/settings.component.html | 8 ++++---- src/app/pages/settings/settings.component.ts | 4 ++-- src/environments/environment.ts | 1 + 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/app/pages/settings/settings.component.html b/src/app/pages/settings/settings.component.html index 40d370b..8dc42aa 100644 --- a/src/app/pages/settings/settings.component.html +++ b/src/app/pages/settings/settings.component.html @@ -28,7 +28,7 @@

Enable Cookies/Storage

+ (checkedChange)="onStateChanged('st-cookies-accepted', $event)">
@@ -39,7 +39,7 @@

Enable Dark Mode

+ (checkedChange)="onStateChanged('st-dark-mode-enabled', $event)">
@@ -50,7 +50,7 @@

Enable Tracking

+ (checkedChange)="onStateChanged('st-enable-tracking', $event)">
@@ -61,7 +61,7 @@

Enable Alerts

+ (checkedChange)="onStateChanged('st-enable-alerts', $event)">
diff --git a/src/app/pages/settings/settings.component.ts b/src/app/pages/settings/settings.component.ts index 2813aa1..0091ac3 100644 --- a/src/app/pages/settings/settings.component.ts +++ b/src/app/pages/settings/settings.component.ts @@ -101,7 +101,7 @@ export class SettingsComponent implements OnInit, OnDestroy { /** * Called when a user changes any of the settings. */ - onStateChanged(key: string) { - this.safeStorageService.save(key, this.enableStorage()); + onStateChanged(key: string, value: any) { + this.safeStorageService.save(key, value); } } diff --git a/src/environments/environment.ts b/src/environments/environment.ts index f9b7917..e292e2b 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -29,6 +29,7 @@ export const environment = { privacy_policy_email: 'info@codeplay.com', fathom_analytics_token: 'MMWGQHXZ', + // A list of any storage keys/cookies that this site uses allowed_storage_keys: [ 'st-cookies-accepted', 'st-dark-mode-enabled', From ba9c45fc7905eeb8c93c4c7818e4db4071f8caf8 Mon Sep 17 00:00:00 2001 From: Scott Straughan Date: Fri, 27 Sep 2024 13:44:04 +0100 Subject: [PATCH 27/27] Small tweak to make date slightly more useful. --- src/app/pages/changed/changed.component.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/app/pages/changed/changed.component.ts b/src/app/pages/changed/changed.component.ts index a714008..506deaa 100644 --- a/src/app/pages/changed/changed.component.ts +++ b/src/app/pages/changed/changed.component.ts @@ -163,8 +163,7 @@ export class ChangedComponent implements OnInit { this.reload(); } else { lastVisitDate = new Date(); - lastVisitDate.setMonth(lastVisitDate.getMonth() - 5); - lastVisitDate.setDate(1); + lastVisitDate.setDate(lastVisitDate.getDate() - 14); this.startDate.set(lastVisitDate); this.onDateSelectorClicked();