diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 6df4ae66be..7dde45fa48 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -612,6 +612,7 @@ deploy_review_b2b: -e LOGGING=true -e SENTRY_DSN=${SENTRY_DSN} -e ICM_BASE_URL=${ICM_BASE_URL} + -e THEME=blue -e ICM_CHANNEL=inSPIRED-inTRONICS_Business-Site -e FEATURES=quoting,recently,compare,businessCustomerRegistration,advancedVariationHandling,sentry --add-host $ICM_HOST:$ICM_IP diff --git a/nginx/README.md b/nginx/README.md index ae6ac65634..2cee977770 100644 --- a/nginx/README.md +++ b/nginx/README.md @@ -28,6 +28,7 @@ Setup at least one PWA channel configuration: - use optional `PWA_X_APPLICATION` for the application name - use optional `PWA_X_LANG` for the default locale in the form of `lang_COUNTRY` - use optional `PWA_X_FEATURES` for a comma separated list of active feature toggles +- use optional `PWA_X_THEME` for setting the theme of the channel Temper with the default Page Speed configuration: diff --git a/nginx/channel.conf.tmpl b/nginx/channel.conf.tmpl index 264eaa4233..6ed1bbf783 100644 --- a/nginx/channel.conf.tmpl +++ b/nginx/channel.conf.tmpl @@ -29,7 +29,7 @@ server { if (-f /etc/nginx/conf.d/icm.conf) { rewrite ^(?!/INTERSHOP.*$)(?!/assets.*$)(?!.*\.js$)(?!.*\.css$)(?!.*\.ico$)(?!.*\.json$)(?!.*\.txt$)(?!.*\.webmanifest$)(.*)$ "$1;icmScheme=$scheme;icmHost=$http_host"; } - rewrite ^(?!/INTERSHOP.*$)(?!/assets.*$)(?!.*\.js$)(?!.*\.css$)(?!.*\.ico$)(?!.*\.json$)(?!.*\.txt$)(?!.*\.webmanifest$)(.*)$ "$1;channel=$CHANNEL;application=$APPLICATION;features=$FEATURES" break; + rewrite ^(?!/INTERSHOP.*$)(?!/assets.*$)(?!.*\.js$)(?!.*\.css$)(?!.*\.ico$)(?!.*\.json$)(?!.*\.txt$)(?!.*\.webmanifest$)(.*)$ "$1;channel=$CHANNEL;application=$APPLICATION;features=$FEATURES;theme=$THEME" break; proxy_pass $UPSTREAM_PWA; } diff --git a/nginx/entrypoint.sh b/nginx/entrypoint.sh index fbff5c3b5b..6f10b5c2c3 100644 --- a/nginx/entrypoint.sh +++ b/nginx/entrypoint.sh @@ -25,10 +25,11 @@ do eval "export APPLICATION=\${PWA_${i}_APPLICATION:-'-'}" eval "export LANG=\${PWA_${i}_LANG:-'default'}" eval "export FEATURES=\${PWA_${i}_FEATURES:-'default'}" + eval "export THEME=\${PWA_${i}_THEME:-''}" echo "$i SUBDOMAIN=$SUBDOMAIN CHANNEL=$CHANNEL APPLICATION=$APPLICATION LANG=$LANG FEATURES=$FEATURES" - envsubst '$UPSTREAM_PWA,$SUBDOMAIN,$CHANNEL,$APPLICATION,$LANG,$FEATURES,$ICM_INCLUDE' /etc/nginx/conf.d/channel$i.conf + envsubst '$UPSTREAM_PWA,$SUBDOMAIN,$CHANNEL,$APPLICATION,$LANG,$FEATURES,$THEME,$ICM_INCLUDE' /etc/nginx/conf.d/channel$i.conf i=$((i+1)) done diff --git a/src/app/core/configuration.module.ts b/src/app/core/configuration.module.ts index f04e61d017..a6c98afee7 100644 --- a/src/app/core/configuration.module.ts +++ b/src/app/core/configuration.module.ts @@ -9,6 +9,7 @@ import { environment } from '../../environments/environment'; import * as injectionKeys from './configurations/injection-keys'; import { FeatureToggleModule } from './feature-toggle.module'; +import { ThemeService } from './utils/theme/theme.service'; @NgModule({ imports: [FeatureToggleModule], @@ -34,10 +35,12 @@ import { FeatureToggleModule } from './feature-toggle.module'; { provide: injectionKeys.MEDIUM_BREAKPOINT_WIDTH, useValue: environment.mediumBreakpointWidth }, { provide: injectionKeys.LARGE_BREAKPOINT_WIDTH, useValue: environment.largeBreakpointWidth }, { provide: injectionKeys.EXTRALARGE_BREAKPOINT_WIDTH, useValue: environment.extralargeBreakpointWidth }, + { provide: injectionKeys.THEME, useValue: environment.theme }, ], }) export class ConfigurationModule { - constructor(@Inject(LOCALE_ID) lang: string, translateService: TranslateService) { + constructor(@Inject(LOCALE_ID) lang: string, translateService: TranslateService, themeService: ThemeService) { + themeService.init(); registerLocaleData(localeDe); registerLocaleData(localeFr); diff --git a/src/app/core/configurations/injection-keys.ts b/src/app/core/configurations/injection-keys.ts index 073ba3a2e1..10eb77f14b 100644 --- a/src/app/core/configurations/injection-keys.ts +++ b/src/app/core/configurations/injection-keys.ts @@ -45,3 +45,8 @@ export const EXTRALARGE_BREAKPOINT_WIDTH = new InjectionToken('extralarg * The captcha configuration siteKey */ export const CAPTCHA_SITE_KEY = new InjectionToken('captchaSiteKey'); + +/** + * The configured theme for the application (or 'default' if not configured) + */ +export const THEME = new InjectionToken('theme'); diff --git a/src/app/core/store/configuration/configuration.effects.ts b/src/app/core/store/configuration/configuration.effects.ts index 90f380750d..31c9162a4d 100644 --- a/src/app/core/store/configuration/configuration.effects.ts +++ b/src/app/core/store/configuration/configuration.effects.ts @@ -49,11 +49,12 @@ export class ConfigurationEffects { this.stateProperties.getStateOrEnvOrDefault('ICM_APPLICATION', 'icmApplication'), this.stateProperties .getStateOrEnvOrDefault('FEATURES', 'features') - .pipe(map(x => (typeof x === 'string' ? x.split(/,/g) : x))) + .pipe(map(x => (typeof x === 'string' ? x.split(/,/g) : x))), + this.stateProperties.getStateOrEnvOrDefault('THEME', 'theme').pipe(map(x => x || 'default')) ), map( - ([, baseURL, server, serverStatic, channel, application, features]) => - new ApplyConfiguration({ baseURL, server, serverStatic, channel, application, features }) + ([, baseURL, server, serverStatic, channel, application, features, theme]) => + new ApplyConfiguration({ baseURL, server, serverStatic, channel, application, features, theme }) ) ); @@ -87,6 +88,10 @@ export class ConfigurationEffects { } } + if (paramMap.has('theme')) { + properties.theme = paramMap.get('theme'); + } + return Object.keys(properties).length ? [new ApplyConfiguration(properties)] : []; } diff --git a/src/app/core/store/configuration/configuration.reducer.ts b/src/app/core/store/configuration/configuration.reducer.ts index 584ddae1f1..9f90e06e4e 100644 --- a/src/app/core/store/configuration/configuration.reducer.ts +++ b/src/app/core/store/configuration/configuration.reducer.ts @@ -8,6 +8,7 @@ export interface ConfigurationState { application?: string; features?: string[]; gtmToken?: string; + theme?: string; } const initialState: ConfigurationState = { @@ -18,6 +19,7 @@ const initialState: ConfigurationState = { application: undefined, features: [], gtmToken: undefined, + theme: undefined, }; export function configurationReducer(state = initialState, action: ConfigurationAction): ConfigurationState { diff --git a/src/app/core/store/configuration/configuration.selectors.ts b/src/app/core/store/configuration/configuration.selectors.ts index 39c271306d..dca63d81bb 100644 --- a/src/app/core/store/configuration/configuration.selectors.ts +++ b/src/app/core/store/configuration/configuration.selectors.ts @@ -46,3 +46,8 @@ export const getGTMToken = createSelector( getConfigurationState, state => state.gtmToken ); + +export const getTheme = createSelector( + getConfigurationState, + state => state.theme +); diff --git a/src/app/core/utils/theme/theme.service.ts b/src/app/core/utils/theme/theme.service.ts new file mode 100644 index 0000000000..c863739b80 --- /dev/null +++ b/src/app/core/utils/theme/theme.service.ts @@ -0,0 +1,56 @@ +import { DOCUMENT } from '@angular/common'; +import { Inject, Injectable, Renderer2, RendererFactory2 } from '@angular/core'; +import { Store, select } from '@ngrx/store'; +import { distinctUntilChanged, filter } from 'rxjs/operators'; + +import { getTheme } from 'ish-core/store/configuration'; + +/** + * Service to add the configured/selected theme’s CSS file in the HTML’s head. + * + * See: "Angular: Multiple Themes Without Killing Bundle Size (With Material or Not)" by @Kmathy15 + * https://medium.com/better-programming/angular-multiple-themes-without-killing-bundle-size-with-material-or-not-5a80849b6b34 + */ +@Injectable({ providedIn: 'root' }) +export class ThemeService { + private renderer: Renderer2; + private head: HTMLElement; + private themeLinks: HTMLElement[] = []; + + constructor( + private rendererFactory: RendererFactory2, + @Inject(DOCUMENT) private document: Document, + private store: Store<{}> + ) {} + + init() { + this.head = this.document.head; + this.renderer = this.rendererFactory.createRenderer(undefined, undefined); + this.store + .pipe(select(getTheme)) + .pipe( + distinctUntilChanged(), + filter(x => !!x) + ) + .subscribe(async theme => { + await this.loadCss(`${theme}.css`); + + // remove style of previous theme + if (this.themeLinks.length === 2) { + this.renderer.removeChild(this.head, this.themeLinks.shift()); + } + }); + } + + private async loadCss(filename: string) { + return new Promise(resolve => { + const linkEl: HTMLElement = this.renderer.createElement('link'); + this.renderer.setAttribute(linkEl, 'rel', 'stylesheet'); + this.renderer.setAttribute(linkEl, 'type', 'text/css'); + this.renderer.setAttribute(linkEl, 'href', filename); + this.renderer.setProperty(linkEl, 'onload', resolve); + this.renderer.appendChild(this.head, linkEl); + this.themeLinks = [...this.themeLinks, linkEl]; + }); + } +} diff --git a/src/environments/environment.model.ts b/src/environments/environment.model.ts index 645b9b2e6e..e73912b77f 100644 --- a/src/environments/environment.model.ts +++ b/src/environments/environment.model.ts @@ -73,4 +73,7 @@ export interface Environment { // configuration of the available locales - hard coded for now locales: Locale[]; + + // configuration of the styling theme ('default' if not configured) + theme?: string; }