diff --git a/README.md b/README.md index 3ac64b85d..00c3ada27 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,9 @@ Squidex is an open source headless CMS and content management hub. In contrast to a traditional CMS Squidex provides a rich API with OData filter and Swagger definitions. It is up to you to build your UI on top of it. It can be website, a native app or just another server. We build it with ASP.NET Core and CQRS and is tested for Windows and Linux on modern browsers. -[![Gitter](https://img.shields.io/gitter/room/nwjs/nw.js.svg?style=square)](https://gitter.im/squidex-cms/Lobby) [![Build Status](http://build.squidex.io/api/badges/Squidex/squidex/status.svg)](http://build.squidex.io/Squidex/squidex) +[![Gitter](https://img.shields.io/gitter/room/nwjs/nw.js.svg?style=square)](https://gitter.im/squidex-cms/Lobby) +[![Slack](https://img.shields.io/badge/chat-on_slack-E01765.svg?style=square)](https://squidex.slack.com/signup) +[![Build Status](http://build.squidex.io/api/badges/Squidex/squidex/status.svg)](http://build.squidex.io/Squidex/squidex) Read the docs at [https://docs.squidex.io/](https://docs.squidex.io/) (work in progress) or just check out the code and play around. diff --git a/src/Squidex/app/features/apps/declarations.ts b/src/Squidex/app/features/apps/declarations.ts index a28600e77..09db639a2 100644 --- a/src/Squidex/app/features/apps/declarations.ts +++ b/src/Squidex/app/features/apps/declarations.ts @@ -5,4 +5,5 @@ * Copyright (c) Sebastian Stehle. All rights reserved */ -export * from './pages/apps-page.component'; \ No newline at end of file +export * from './pages/apps-page.component'; +export * from './pages/onboarding-dialog.component'; \ No newline at end of file diff --git a/src/Squidex/app/features/apps/module.ts b/src/Squidex/app/features/apps/module.ts index 0fed8a78e..72fba97a3 100644 --- a/src/Squidex/app/features/apps/module.ts +++ b/src/Squidex/app/features/apps/module.ts @@ -11,7 +11,8 @@ import { RouterModule, Routes } from '@angular/router'; import { SqxFrameworkModule, SqxSharedModule } from 'shared'; import { - AppsPageComponent + AppsPageComponent, + OnboardingDialogComponent } from './declarations'; const routes: Routes = [ @@ -28,7 +29,8 @@ const routes: Routes = [ RouterModule.forChild(routes) ], declarations: [ - AppsPageComponent + AppsPageComponent, + OnboardingDialogComponent ] }) export class SqxFeatureAppsModule { } \ No newline at end of file diff --git a/src/Squidex/app/features/apps/pages/apps-page.component.html b/src/Squidex/app/features/apps/pages/apps-page.component.html index 92a9c4efc..e485a137e 100644 --- a/src/Squidex/app/features/apps/pages/apps-page.component.html +++ b/src/Squidex/app/features/apps/pages/apps-page.component.html @@ -35,4 +35,6 @@ - \ No newline at end of file + + + \ No newline at end of file diff --git a/src/Squidex/app/features/apps/pages/apps-page.component.ts b/src/Squidex/app/features/apps/pages/apps-page.component.ts index 4de9a5afc..741dc448f 100644 --- a/src/Squidex/app/features/apps/pages/apps-page.component.ts +++ b/src/Squidex/app/features/apps/pages/apps-page.component.ts @@ -5,12 +5,14 @@ * Copyright (c) Sebastian Stehle. All rights reserved */ -import { Component, OnInit } from '@angular/core'; +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { Subscription } from 'rxjs'; import { AppsStoreService, fadeAnimation, - ModalView + ModalView, + OnboardingService } from 'shared'; @Component({ @@ -21,17 +23,34 @@ import { fadeAnimation ] }) -export class AppsPageComponent implements OnInit { - public addAppDialog = new ModalView(); +export class AppsPageComponent implements OnDestroy, OnInit { + private onboardingAppsSubscription: Subscription; + public addAppDialog = new ModalView(); public apps = this.appsStore.apps; + public onboardingModal = new ModalView(); + constructor( - private readonly appsStore: AppsStoreService + private readonly appsStore: AppsStoreService, + private readonly onboardingService: OnboardingService ) { } + public ngOnDestroy() { + this.onboardingAppsSubscription.unsubscribe(); + } + public ngOnInit() { this.appsStore.selectApp(null); + + this.onboardingAppsSubscription = + this.appsStore.apps + .subscribe(apps => { + if (apps.length === 0 && this.onboardingService.shouldShow('dialog')) { + this.onboardingService.disable('dialog'); + this.onboardingModal.show(); + } + }); } } \ No newline at end of file diff --git a/src/Squidex/app/features/apps/pages/onboarding-dialog.component.html b/src/Squidex/app/features/apps/pages/onboarding-dialog.component.html new file mode 100644 index 000000000..9e3d5ed46 --- /dev/null +++ b/src/Squidex/app/features/apps/pages/onboarding-dialog.component.html @@ -0,0 +1,154 @@ + \ No newline at end of file diff --git a/src/Squidex/app/features/apps/pages/onboarding-dialog.component.scss b/src/Squidex/app/features/apps/pages/onboarding-dialog.component.scss new file mode 100644 index 000000000..7be768784 --- /dev/null +++ b/src/Squidex/app/features/apps/pages/onboarding-dialog.component.scss @@ -0,0 +1,78 @@ +@import '_vars'; +@import '_mixins'; + +$size-width: 825px; +$size-height: 576px; + +$size-image: 476px; + +h1 { + font-size: 1.6rem; +} + +p { + line-height: 1.8rem; +} + +.modal { + &-content, + &-dialog { + min-height: $size-height; + max-height: $size-height; + min-width: $size-width; + max-width: $size-width; + } + + &-content { + color: $color-dark-foreground; + background-color: $color-dark-onboarding; + background-image: url('/images/onboarding-background.png'); + overflow: hidden; + position: relative; + } + + &-close { + text-decoration: underline !important; + cursor: pointer; + color: $color-dark-foreground; + } +} + +.header-focus { + color: $color-theme-blue; +} + +.header-left { + @include absolute(-70px, auto, auto, 2rem); +} + +.header-right { + @include absolute(2rem, 2rem, auto, auto); +} + +.footer { + @include absolute(auto, auto, 5rem, 2rem); +} + +.onboarding { + &-enter-leave { + & { + text-align: center; + margin: 4rem auto 0; + max-width: 28rem; + min-width: 28rem; + } + + p { + margin: 2rem 0; + } + } + + &-text { + padding-left: 2rem; + } + + &-step { + @include absolute($size-height - $size-image, 0, 0, 0); + } +} \ No newline at end of file diff --git a/src/Squidex/app/features/apps/pages/onboarding-dialog.component.ts b/src/Squidex/app/features/apps/pages/onboarding-dialog.component.ts new file mode 100644 index 000000000..ce057ea62 --- /dev/null +++ b/src/Squidex/app/features/apps/pages/onboarding-dialog.component.ts @@ -0,0 +1,33 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +import { Component, Input } from '@angular/core'; + +import { + fadeAnimation, + ModalView, + slideAnimation +} from 'framework'; + +@Component({ + selector: 'sqx-onboarding-dialog', + styleUrls: ['./onboarding-dialog.component.scss'], + templateUrl: './onboarding-dialog.component.html', + animations: [ + fadeAnimation, slideAnimation + ] +}) +export class OnboardingDialogComponent { + public step = 0; + + @Input() + public modalView = new ModalView(); + + public next() { + this.step = this.step + 1; + } +} \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/animations.ts b/src/Squidex/app/framework/angular/animations.ts index 596d76fc7..df32c33b1 100644 --- a/src/Squidex/app/framework/angular/animations.ts +++ b/src/Squidex/app/framework/angular/animations.ts @@ -37,6 +37,29 @@ export function buildSlideRightAnimation(name = 'slideRight', timing = '150ms'): ); } +export function buildSlideAnimation(name = 'slide', timing = '400ms'): AnimationEntryMetadata { + return trigger( + name, [ + transition(':enter', [ + style({ transform: 'translateX(100%)' }), + animate(timing, style({ transform: 'translateX(0%)' })) + ]), + transition(':leave', [ + style({transform: 'translateX(0%)' }), + animate(timing, style({ transform: 'translateX(-100%)' })) + ]), + state('true', + style({ transform: 'translateX(0%)' }) + ), + state('false', + style({ transform: 'translateX(-100%)' }) + ), + transition('1 => 0', animate(timing)), + transition('0 => 1', animate(timing)) + ] + ); +} + export function buildFadeAnimation(name = 'fade', timing = '150ms'): AnimationEntryMetadata { return trigger( name, [ @@ -85,4 +108,5 @@ export function buildHeightAnimation(name = 'height', timing = '200ms'): Animati export const fadeAnimation = buildFadeAnimation(); export const heightAnimation = buildHeightAnimation(); +export const slideAnimation = buildSlideAnimation(); export const slideRightAnimation = buildSlideRightAnimation(); diff --git a/src/Squidex/app/framework/angular/modal-view.directive.ts b/src/Squidex/app/framework/angular/modal-view.directive.ts index adceb0fad..4905636a5 100644 --- a/src/Squidex/app/framework/angular/modal-view.directive.ts +++ b/src/Squidex/app/framework/angular/modal-view.directive.ts @@ -26,6 +26,9 @@ export class ModalViewDirective implements OnChanges, OnDestroy { @Input('sqxModalViewOnRoot') public placeOnRoot = false; + @Input('sqxModalViewCloseAuto') + public closeAuto = true; + constructor( private readonly templateRef: TemplateRef, private readonly renderer: Renderer, @@ -84,6 +87,10 @@ export class ModalViewDirective implements OnChanges, OnDestroy { } private startListening() { + if (!this.closeAuto) { + return; + } + this.documentClickListener = this.renderer.listenGlobal('document', 'click', (event: MouseEvent) => { if (!event.target || this.renderedView === null) { diff --git a/src/Squidex/app/framework/services/onboarding.service.spec.ts b/src/Squidex/app/framework/services/onboarding.service.spec.ts new file mode 100644 index 000000000..1f6de9570 --- /dev/null +++ b/src/Squidex/app/framework/services/onboarding.service.spec.ts @@ -0,0 +1,70 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +import { OnboardingService, OnboardingServiceFactory } from './../'; + +class LocalStoreMock { + private store = {}; + + public get(key: string) { + return this.store[key]; + } + + public set(key: string, value: string) { + this.store[key] = value; + } +} + +describe('OnboardingService', () => { + const localStore = new LocalStoreMock(); + + it('should instantiate from factory', () => { + const onboardingService = OnboardingServiceFactory(localStore); + + expect(onboardingService).toBeDefined(); + }); + + it('should instantiate', () => { + const onboardingService = new OnboardingService(localStore); + + expect(onboardingService).toBeDefined(); + }); + + it('should return true when value not in store', () => { + localStore.set('squidex.onboarding.disable.feature1', '0'); + + const onboardingService = new OnboardingService(localStore); + + onboardingService.disable('feature2'); + + expect(onboardingService.shouldShow('feature1')).toBeTruthy(); + }); + + it('should return false when value in store', () => { + localStore.set('squidex.onboarding.disable.feature1', '1'); + + const onboardingService = new OnboardingService(localStore); + + expect(onboardingService.shouldShow('feature1')).toBeFalsy(); + }); + + it('should return false when disabled', () => { + const onboardingService = new OnboardingService(localStore); + + onboardingService.disable('feature1'); + + expect(onboardingService.shouldShow('feature1')).toBeFalsy(); + }); + + it('should return false when all disabled', () => { + const onboardingService = new OnboardingService(localStore); + + onboardingService.disableAll(); + + expect(onboardingService.shouldShow('feature1')).toBeFalsy(); + }); +}); \ No newline at end of file diff --git a/src/Squidex/app/theme/_bootstrap.scss b/src/Squidex/app/theme/_bootstrap.scss index 3dcfdf5ad..269cfab84 100644 --- a/src/Squidex/app/theme/_bootstrap.scss +++ b/src/Squidex/app/theme/_bootstrap.scss @@ -71,6 +71,12 @@ a { @include opacity(.8); pointer-events: none; } + + &.btn { + &:focus { + color: inherit; + } + } } // diff --git a/src/Squidex/app/theme/_vars.scss b/src/Squidex/app/theme/_vars.scss index 187aafb58..f2e31dfba 100644 --- a/src/Squidex/app/theme/_vars.scss +++ b/src/Squidex/app/theme/_vars.scss @@ -53,6 +53,8 @@ $color-dark2-control: #2e3842; $color-dark2-separator: #2e3842; $color-dark2-placeholder: #757e8d; +$color-dark-onboarding: #2d333c; + $color-panel-icon: #a2b0b6; $color-badge-success-background: #e4f6e6; diff --git a/src/Squidex/wwwroot/images/logo-white-small.png b/src/Squidex/wwwroot/images/logo-white-small.png new file mode 100644 index 000000000..3aee0c55e Binary files /dev/null and b/src/Squidex/wwwroot/images/logo-white-small.png differ diff --git a/src/Squidex/wwwroot/images/onboarding-background.png b/src/Squidex/wwwroot/images/onboarding-background.png new file mode 100644 index 000000000..a3911adde Binary files /dev/null and b/src/Squidex/wwwroot/images/onboarding-background.png differ diff --git a/src/Squidex/wwwroot/images/onboarding-step1.png b/src/Squidex/wwwroot/images/onboarding-step1.png new file mode 100644 index 000000000..9e733de40 Binary files /dev/null and b/src/Squidex/wwwroot/images/onboarding-step1.png differ diff --git a/src/Squidex/wwwroot/images/onboarding-step2.png b/src/Squidex/wwwroot/images/onboarding-step2.png new file mode 100644 index 000000000..f2c2545b1 Binary files /dev/null and b/src/Squidex/wwwroot/images/onboarding-step2.png differ diff --git a/src/Squidex/wwwroot/images/onboarding-step3.png b/src/Squidex/wwwroot/images/onboarding-step3.png new file mode 100644 index 000000000..7d904fa6f Binary files /dev/null and b/src/Squidex/wwwroot/images/onboarding-step3.png differ diff --git a/src/Squidex/wwwroot/images/onboarding-step4.png b/src/Squidex/wwwroot/images/onboarding-step4.png new file mode 100644 index 000000000..9bc391ff1 Binary files /dev/null and b/src/Squidex/wwwroot/images/onboarding-step4.png differ