diff --git a/angular.json b/angular.json index 22fa6659..3d033e7b 100644 --- a/angular.json +++ b/angular.json @@ -21,6 +21,7 @@ "src/assets", "src/favicon.ico", "src/favicon.png", + "src/google46533d2e7a851062.html", { "glob": "**/*", "input": "node_modules/leaflet/dist/images", @@ -122,6 +123,7 @@ "src/assets", "src/favicon.ico", "src/favicon.png", + "src/google46533d2e7a851062.html", { "glob": "**/*", "input": "node_modules/leaflet/dist/images", diff --git a/src/app/@core/core.module.ts b/src/app/@core/core.module.ts index 63298734..593c29a8 100644 --- a/src/app/@core/core.module.ts +++ b/src/app/@core/core.module.ts @@ -51,6 +51,7 @@ import { StatsProgressBarService } from './mock/stats-progress-bar.service'; import { VisitorsAnalyticsService } from './mock/visitors-analytics.service'; import { SecurityCamerasService } from './mock/security-cameras.service'; import { MockDataModule } from './mock/mock-data.module'; +import { AbService } from './utils/ab.service'; const socialLinks = [ { @@ -141,6 +142,7 @@ export const NB_CORE_PROVIDERS = [ LayoutService, PlayerService, StateService, + AbService, ]; @NgModule({ diff --git a/src/app/@core/utils/ab.service.ts b/src/app/@core/utils/ab.service.ts new file mode 100644 index 00000000..8388ad6c --- /dev/null +++ b/src/app/@core/utils/ab.service.ts @@ -0,0 +1,43 @@ +import { Injectable } from '@angular/core'; + +import { fromEvent as observableFromEvent } from 'rxjs/observable/fromEvent'; +import { BehaviorSubject } from 'rxjs/BehaviorSubject'; +import { Observable } from 'rxjs/Observable'; +import { filter } from 'rxjs/operators/filter'; + +@Injectable() +export class AbService { + + static readonly VARIANT_THEME_DEFAULT = 'theme-change-default'; + static readonly VARIANT_THEME_COSMIC = 'theme-change-cosmic'; + static readonly VARIANT_THEME_CORPORATE = 'theme-change-corporate'; + static readonly VARIANT_HIGHLIGHT_HIRE = 'highlight-hire'; + static readonly VARIANT_DEVELOPERS_HIRE = 'developers-hire'; + static readonly VARIANT_SOLUTION_HIRE = 'solution-hire'; + // static readonly VARIANT_BANNER_HIRE = 'banner-hire'; + + private static readonly EVENT_NAME = 'ab-variant'; + private static readonly AB_ENABLED = true; + + private events$ = new BehaviorSubject<{ name: string }>(null); + + constructor() { + + if (AbService.AB_ENABLED) { + observableFromEvent(document, AbService.EVENT_NAME) + .subscribe((e: { detail: any }) => { + if (e && e.detail) { + this.events$.next(e.detail); + } + }); + } + } + + onAbEvent(name: string = ''): Observable<{ name: string }> { + return this.events$.asObservable() + .pipe( + filter(e => !!(e && e.name)), + filter(e => name ? e.name === name : true), + ); + } +} diff --git a/src/app/@core/utils/analytics.service.ts b/src/app/@core/utils/analytics.service.ts index 20dcc02c..2cb0b271 100644 --- a/src/app/@core/utils/analytics.service.ts +++ b/src/app/@core/utils/analytics.service.ts @@ -1,16 +1,19 @@ -import { Injectable } from '@angular/core'; +import { Inject, Injectable } from '@angular/core'; import { NavigationEnd, Router } from '@angular/router'; import { Location } from '@angular/common'; import { filter } from 'rxjs/operators'; +import { NB_WINDOW } from '@nebular/theme'; declare const ga: any; @Injectable() export class AnalyticsService { - private enabled: boolean; + private enabled = false; - constructor(private location: Location, private router: Router) { - this.enabled = false; + constructor(@Inject(NB_WINDOW) private window, + private location: Location, + private router: Router) { + this.enabled = this.window.location.href.indexOf('akveo.com') >= 0; } trackPageViews() { @@ -24,9 +27,9 @@ export class AnalyticsService { } } - trackEvent(eventName: string) { + trackEvent(eventName: string, eventVal: string = '') { if (this.enabled) { - ga('send', 'event', eventName); + ga('send', 'event', eventName, eventVal); } } } diff --git a/src/app/@theme/components/footer/footer.component.ts b/src/app/@theme/components/footer/footer.component.ts index b1fb1c40..cf11d789 100644 --- a/src/app/@theme/components/footer/footer.component.ts +++ b/src/app/@theme/components/footer/footer.component.ts @@ -4,12 +4,21 @@ import { Component } from '@angular/core'; selector: 'ngx-footer', styleUrls: ['./footer.component.scss'], template: ` - Created with ♥ by Akveo 2017 + + Created with ♥ by Akveo 2018. + Made with + + + Nebular. + + +
- - - - + + + +
`, }) diff --git a/src/app/@theme/components/header/header.component.html b/src/app/@theme/components/header/header.component.html index 1ab9eef1..7f76e567 100644 --- a/src/app/@theme/components/header/header.component.html +++ b/src/app/@theme/components/header/header.component.html @@ -17,10 +17,25 @@ - - - + + + - + + + + + + + + diff --git a/src/app/@theme/components/header/header.component.scss b/src/app/@theme/components/header/header.component.scss index 1c7a2c72..73cd4e84 100644 --- a/src/app/@theme/components/header/header.component.scss +++ b/src/app/@theme/components/header/header.component.scss @@ -12,10 +12,12 @@ width: 100%; order: 0; flex-direction: row; + flex: 1; } .right { order: 1; flex-direction: row-reverse; + flex: 1.5; } .logo-containter { @@ -26,6 +28,22 @@ .control-item { display: block; + + &.email-text { + height: auto; + a { + text-decoration: underline; + } + } + + &.github-start { + width: 140px; + display: flex; + + iframe { + width: 100px; + } + } } .header-container { @@ -153,6 +171,19 @@ } } + @include media-breakpoint-down(xl) { + + .control-item.email, .control-item.notifications { + display: none; + } + } + + @include media-breakpoint-down(lg) { + .control-item.search { + display: none; + } + } + @include media-breakpoint-down(md) { nb-action:not(.toggle-settings) { @@ -181,9 +212,10 @@ nb-user /deep/ .user-name { display: none; } - } - @include media-breakpoint-down(is) { + .control-item.github-start { + display: none; + } .header-container { .logo { diff --git a/src/app/@theme/components/header/header.component.ts b/src/app/@theme/components/header/header.component.ts index 788802b2..861569b5 100644 --- a/src/app/@theme/components/header/header.component.ts +++ b/src/app/@theme/components/header/header.component.ts @@ -1,8 +1,11 @@ -import { Component, Input, OnInit } from '@angular/core'; +import { Component, Input, OnDestroy, OnInit } from '@angular/core'; import { NbMenuService, NbSidebarService } from '@nebular/theme'; import { UserData } from '../../../@core/data/users'; -import { AnalyticsService } from '../../../@core/utils'; +import { AnalyticsService } from '../../../@core/utils/analytics.service'; +import { takeWhile } from 'rxjs/operators/takeWhile'; +import { fromEvent as observableFromEvent } from 'rxjs/observable/fromEvent'; +import { AbService } from '../../../@core/utils/ab.service'; import { LayoutService } from '../../../@core/utils'; @Component({ @@ -10,24 +13,44 @@ import { LayoutService } from '../../../@core/utils'; styleUrls: ['./header.component.scss'], templateUrl: './header.component.html', }) -export class HeaderComponent implements OnInit { +export class HeaderComponent implements OnInit , OnDestroy { @Input() position = 'normal'; user: any; - + hireTextVariant: string = 'solution-hire'; userMenu = [{ title: 'Profile' }, { title: 'Log out' }]; + private alive = true; + constructor(private sidebarService: NbSidebarService, private menuService: NbMenuService, private userService: UserData, - private analyticsService: AnalyticsService, + private analytics: AnalyticsService, + private abService: AbService, private layoutService: LayoutService) { + + observableFromEvent(document, 'mouseup') + .pipe(takeWhile(() => this.alive)) + .subscribe(() => { + let selection: any; + if (window.getSelection) { + selection = window.getSelection(); + } else if (( document).selection) { + selection = ( document).selection.createRange(); + } + + if (selection && selection.toString() === 'contact@akveo.com') { + this.analytics.trackEvent('contactEmail', 'select'); + } + }); } ngOnInit() { this.userService.getUsers() .subscribe((users: any) => this.user = users.nick); + + this.listenForVariants(); } toggleSidebar(): boolean { @@ -42,6 +65,29 @@ export class HeaderComponent implements OnInit { } startSearch() { - this.analyticsService.trackEvent('startSearch'); + this.analytics.trackEvent('startSearch'); + } + + trackEmailClick() { + this.analytics.trackEvent('contactEmail', 'click'); + } + + ngOnDestroy() { + this.alive = false; + } + + listenForVariants() { + const variants = [ + AbService.VARIANT_DEVELOPERS_HIRE, + AbService.VARIANT_HIGHLIGHT_HIRE, + AbService.VARIANT_SOLUTION_HIRE, + ]; + + this.abService.onAbEvent() + .subscribe((e: { name: string }) => { + if (variants.includes(e.name)) { + this.hireTextVariant = e.name; + } + }); } } diff --git a/src/app/@theme/components/layout-direction-switcher/layout-direction-switcher.component.ts b/src/app/@theme/components/layout-direction-switcher/layout-direction-switcher.component.ts index 1c3267d0..eaf40bb2 100644 --- a/src/app/@theme/components/layout-direction-switcher/layout-direction-switcher.component.ts +++ b/src/app/@theme/components/layout-direction-switcher/layout-direction-switcher.component.ts @@ -1,6 +1,7 @@ import { Component, OnDestroy, Input } from '@angular/core'; import { NbLayoutDirectionService, NbLayoutDirection } from '@nebular/theme'; import { takeWhile } from 'rxjs/operators'; +import { AnalyticsService } from '../../../@core/utils/analytics.service'; @Component({ selector: 'ngx-layout-direction-switcher', @@ -24,7 +25,8 @@ export class LayoutDirectionSwitcherComponent implements OnDestroy { @Input() vertical: boolean = false; - constructor(private directionService: NbLayoutDirectionService) { + constructor(private directionService: NbLayoutDirectionService, + private analyticsService: AnalyticsService) { this.currentDirection = this.directionService.getDirection(); this.directionService.onDirectionChange() @@ -34,6 +36,8 @@ export class LayoutDirectionSwitcherComponent implements OnDestroy { toggleDirection(newDirection) { this.directionService.setDirection(newDirection); + + this.analyticsService.trackEvent('toggleDirection', newDirection); } ngOnDestroy() { diff --git a/src/app/@theme/layouts/sample/sample.layout.ts b/src/app/@theme/layouts/sample/sample.layout.ts index 22a4275c..4c334f2a 100644 --- a/src/app/@theme/layouts/sample/sample.layout.ts +++ b/src/app/@theme/layouts/sample/sample.layout.ts @@ -26,7 +26,7 @@ import { StateService } from '../../../@core/utils'; responsive [end]="sidebar.id === 'end'"> - + Support Us diff --git a/src/app/app.component.ts b/src/app/app.component.ts index f858b0c4..3e4d0343 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -4,7 +4,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. */ import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { NbThemeService } from '@nebular/theme'; + import { AnalyticsService } from './@core/utils/analytics.service'; +import { AbService } from './@core/utils/ab.service'; +import { withLatestFrom, filter } from 'rxjs/operators'; @Component({ selector: 'ngx-app', @@ -12,10 +17,45 @@ import { AnalyticsService } from './@core/utils/analytics.service'; }) export class AppComponent implements OnInit { - constructor(private analytics: AnalyticsService) { + themes = ['default', 'cosmic', 'corporate']; + + constructor(private analytics: AnalyticsService, + private activatedRoute: ActivatedRoute, + private abService: AbService, + private themeService: NbThemeService) { + + this.themeService.onThemeChange() + .subscribe((theme: any) => { + this.analytics.trackEvent('themeUsed', theme.name); + }); + + this.activatedRoute.queryParams + .subscribe((params: any) => { + if (params.theme && this.themes.includes(params.theme)) { + this.themeService.changeTheme(params.theme); + } + }); } ngOnInit(): void { + + const variants = [ + AbService.VARIANT_THEME_CORPORATE, + AbService.VARIANT_THEME_DEFAULT, + AbService.VARIANT_THEME_CORPORATE, + ]; + this.analytics.trackPageViews(); + this.abService.onAbEvent() + .pipe( + withLatestFrom(this.activatedRoute.queryParams), + filter(([e, params]: [{ name: string }, any]) => !params.theme), + ) + .subscribe(([e, params]: [{ name: string }, any]) => { + const themeName = e.name.replace('theme-change-', ''); + if (variants.includes(e.name) && this.themes.includes(themeName)) { + this.themeService.changeTheme(themeName); + } + }); } } diff --git a/src/app/pages/pages.component.ts b/src/app/pages/pages.component.ts index b16da939..856a59e5 100644 --- a/src/app/pages/pages.component.ts +++ b/src/app/pages/pages.component.ts @@ -1,4 +1,4 @@ -import { Component } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { MENU_ITEMS } from './pages-menu'; @@ -12,7 +12,13 @@ import { MENU_ITEMS } from './pages-menu'; `, }) -export class PagesComponent { +export class PagesComponent implements OnInit { menu = MENU_ITEMS; + + ngOnInit() { + if (window['dataLayer']) { + window['dataLayer'].push({'event': 'optimize.activate'}); + } + } } diff --git a/src/google46533d2e7a851062.html b/src/google46533d2e7a851062.html new file mode 100644 index 00000000..b311a4fc --- /dev/null +++ b/src/google46533d2e7a851062.html @@ -0,0 +1 @@ +google-site-verification: google46533d2e7a851062.html \ No newline at end of file diff --git a/src/index.html b/src/index.html index 71e67503..ef04d49e 100644 --- a/src/index.html +++ b/src/index.html @@ -2,11 +2,32 @@ - ngx-admin Demo Application + ngx-admin - Angular 6, Bootstrap 4 Admin dashboard template + + + + + + + + Loading...