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

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 42ceabd..485ddaf 100644 --- a/src/app/pages/cookies/cookies.component.ts +++ b/src/app/pages/cookies/cookies.component.ts @@ -19,14 +19,17 @@ 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'; +import { RouterLink } from '@angular/router'; @Component({ selector: 'st-cookies', standalone: true, imports: [ - CommonModule + CommonModule, + RouterLink ], templateUrl: './cookies.component.html', styleUrl: './cookies.component.scss', @@ -37,11 +40,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 +54,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/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; 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/home/home.component.html b/src/app/pages/home/home.component.html index 31cb5d1..8aca804 100644 --- a/src/app/pages/home/home.component.html +++ b/src/app/pages/home/home.component.html @@ -45,6 +45,8 @@

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

News & Updates

+ + updateWhat's Changed? newspaperView All
diff --git a/src/app/pages/home/home.component.ts b/src/app/pages/home/home.component.ts index 2a6b861..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'; @@ -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: [ @@ -80,7 +83,7 @@ import { environment } from '../../../environments/environment'; ], 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; @@ -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, @@ -164,6 +169,27 @@ export class HomeComponent implements SearchablePage { this.researchService.count(), { initialValue: 0 }); } + /** + * @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'); + } + /** * @inheritDoc */ diff --git a/src/app/pages/playground/playground.component.ts b/src/app/pages/playground/playground.component.ts index 707aafb..6392a05 100644 --- a/src/app/pages/playground/playground.component.ts +++ b/src/app/pages/playground/playground.component.ts @@ -42,15 +42,15 @@ 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'; +import { AlertService } from '../../shared/services/alert.service'; @Component({ selector: 'st-playground', @@ -100,12 +100,12 @@ 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 + * @param alertService */ constructor( protected titleService: Title, @@ -113,12 +113,12 @@ 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 + protected router: Router, + protected alertService: AlertService ) { this.titleService.setTitle('Playground - SYCL.tech'); this.meta.addTag({ name: 'keywords', content: this.getKeywords().join(', ') }); @@ -174,7 +174,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 +210,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')); } /** @@ -243,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}.` + }); } }) ); 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..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,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 { 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'; @Component({ selector: 'st-load-popup', @@ -48,36 +47,36 @@ 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); - - if (this.storageService.has(LoadAndSavePopupComponent.storageKey)) { - const saved = this.storageService.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(); } /** * 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 +122,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.html b/src/app/pages/settings/settings.component.html index e029e9f..8dc42aa 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 4859995..0091ac3 100644 --- a/src/app/pages/settings/settings.component.ts +++ b/src/app/pages/settings/settings.component.ts @@ -16,12 +16,12 @@ * *--------------------------------------------------------------------------------------------*/ -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 { StateService } from '../../shared/services/state.service'; -import { tap } from 'rxjs'; +import { Subscription, tap } from 'rxjs'; import { Title } from '@angular/platform-browser'; +import { SafeStorageService } from '../../shared/services/safe-storage.service'; @Component({ selector: 'st-settings', @@ -34,39 +34,74 @@ import { Title } from '@angular/platform-browser'; styleUrl: './settings.component.scss', changeDetection: ChangeDetectionStrategy.OnPush }) -export class SettingsComponent { - enableStorage: WritableSignal = signal(false); - enableDarkMode: WritableSignal = signal(false); - enableTracking: WritableSignal = signal(false); +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; /** * 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( + /** + * @inheritdoc + */ + ngOnInit(): void { + this.storageSubscription = this.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); + this.enableAlerts.set(state['st-enable-alerts'] == true); }) ).subscribe(); } + /** + * @inheritdoc + */ + ngOnDestroy(): void { + this.storageSubscription?.unsubscribe(); + } + /** * 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); + onStateChanged(key: string, value: any) { + this.safeStorageService.save(key, value); } } 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; 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..b25669a --- /dev/null +++ b/src/app/shared/components/site-wide-alert/alerts.component.html @@ -0,0 +1,21 @@ +@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 new file mode 100644 index 0000000..376238a --- /dev/null +++ b/src/app/shared/components/site-wide-alert/alerts.component.scss @@ -0,0 +1,89 @@ +:host { + $side-padding: 1rem; + $container-padding: 1.5rem; + + position: fixed; + display: flex; + flex-direction: column; + gap: 1rem; + bottom: $container-padding; + right: $container-padding; + z-index: 9999; + + * { + margin: 0; + padding: 0; + } + + .container { + width: 430px; + display: flex; + overflow: hidden; + 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 { + box-shadow: 2px 2px 5px 0 rgba(0, 0, 0, .4); + background-color: var(--color-orange); + } + + .content { + display: flex; + padding: $side-padding; + gap: 1rem; + flex: 1; + cursor: pointer; + + .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: .8; + margin-top: .2rem; + } + } + } + + .buttons { + display: flex; + gap: 1rem; + + a { + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + padding: $side-padding; + opacity: .5; + transition: var(--transition-fast); + background-color: rgba(0, 0, 0, .15); + + &:hover { + opacity: 1; + } + } + } + } +} 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..5ee021e --- /dev/null +++ b/src/app/shared/components/site-wide-alert/alerts.component.ts @@ -0,0 +1,120 @@ +/*--------------------------------------------------------------------------------------------- + * + * 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, OnDestroy, + OnInit, + signal, + WritableSignal +} from '@angular/core'; +import { Alert, AlertService } from '../../services/alert.service'; +import { RouterLink } from '@angular/router'; +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'; +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('slideInSide', [ + transition(':enter', [ + style({ transform: 'translateX(150%)', opacity: 0 }), + animate('500ms ease-out', style({ transform: 'translateX(0)', opacity: 1 })), + ]), + transition(':leave', [ + animate('500ms ease-out', style({ transform: 'translateX(150%)', opacity: 0 })), + ]) + ]) +], +}) +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 + * @param safeStorageService + * @param alertService + */ + constructor( + protected platformService: PlatformService, + protected safeStorageService: SafeStorageService, + protected alertService: AlertService + ) { } + + /** + * @inheritdoc + */ + ngOnInit() { + if (!this.platformService.isClient()) { + return; + } + + this.alertSubscription = this.alertService.observe() + .pipe( + tap((alerts) => { + this.alerts.set(alerts); + }) + ) + .subscribe(); + } + + /** + * @inheritdoc + */ + ngOnDestroy() { + this.alertSubscription?.unsubscribe(); + } + + /** + * 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/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(); } } diff --git a/src/app/shared/services/alert.service.ts b/src/app/shared/services/alert.service.ts new file mode 100644 index 0000000..b3fbd21 --- /dev/null +++ b/src/app/shared/services/alert.service.ts @@ -0,0 +1,213 @@ +/*--------------------------------------------------------------------------------------------- + * + * 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 { 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 + */ + protected alerts: Alert[] = []; + + /** + * Subject, used to notify observers of changes to the alerts. + * @protected + */ + protected behaviorSubject: BehaviorSubject = new BehaviorSubject([]); + + /** + * Constructor. + * @param safeStorageService + * @param platformService + */ + constructor( + 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 { + return this.behaviorSubject; + } + + /** + * Add a new alert. + * @param alert + */ + add( + alert: Alert + ) { + // Don't do anything if we are not a client + if (!this.platformService.isClient()) { + return ; + } + + // 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); + } + + /** + * Block an alert. + * @param alert + */ + block( + alert: Alert + ) { + 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.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(); + } + + /** + * 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() { + if (!this.enabled) { + return ; + } + + this.behaviorSubject.next(this.getAlerts()); + } + + /** + * Check if there are alerts available. + */ + has(): boolean { + return this.alerts.length > 0; + } + + /** + * Gets the next alert in the alert list. + */ + getAlerts(): Alert[] { + return this.alerts.slice(); + } +} + +/** + * Represents an alert. + */ +export interface Alert { + id: string + icon: string + title: string + persistent?: boolean + description?: string + href?: string +} diff --git a/src/app/shared/services/changed.service.ts b/src/app/shared/services/changed.service.ts new file mode 100644 index 0000000..0d6f4bd --- /dev/null +++ b/src/app/shared/services/changed.service.ts @@ -0,0 +1,63 @@ +/*--------------------------------------------------------------------------------------------- + * + * 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 + ) { } + + /** + * 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 + } + } +} 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 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..efc481b --- /dev/null +++ b/src/app/shared/services/safe-storage.service.ts @@ -0,0 +1,178 @@ +/*--------------------------------------------------------------------------------------------- + * + * 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; + + protected readonly enableByDefault = [ + 'st-enable-tracking', + 'st-enable-alerts' + ] + + /** + * 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(); + } + } + + /** + * 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(); + } + + /** + * 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 + * @throws DefaultStorageKeys will be thrown if we are not allowed to store data + */ + save( + key: string, + value: any + ) { + // If the key is the STORAGE_ALLOWED_KEY, handle this separately + if (key == SafeStorageService.STORAGE_ALLOWED_KEY) { + this.setStorageAllowed(value); + return ; + } + + // 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); + + // Notify any observers of change to the storage state + 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. + */ + private 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..e292e2b 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -27,5 +27,17 @@ export const environment = { discord: 'https://discord.com/invite/FkGSFA3asN' }, privacy_policy_email: 'info@codeplay.com', - fathom_analytics_token: 'MMWGQHXZ' + 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', + 'st-enable-tracking', + 'st-playground-compiler-tag', + 'st-playground-saved', + 'st-last-visit-date', + 'st-blocked-alerts', + 'st-enable-alerts', + ], };