diff --git a/dmp-frontend/angular.json b/dmp-frontend/angular.json index e3a5fc57b..dbc2640b5 100644 --- a/dmp-frontend/angular.json +++ b/dmp-frontend/angular.json @@ -27,7 +27,8 @@ "src/styles.scss", "src/assets/scss/material-dashboard.scss", "src/assets/css/demo.css", - "node_modules/cookieconsent/build/cookieconsent.min.css" + "node_modules/cookieconsent/build/cookieconsent.min.css", + "src/app/library/guided-tour/guided-tour-base-theme.scss" ], "scripts": [ "node_modules/cookieconsent/build/cookieconsent.min.js", diff --git a/dmp-frontend/package.json b/dmp-frontend/package.json index 38701dc4c..07983c8b1 100644 --- a/dmp-frontend/package.json +++ b/dmp-frontend/package.json @@ -32,6 +32,7 @@ "moment-timezone": "^0.5.26", "ngx-cookie-service": "^2.2.0", "ngx-cookieconsent": "^2.2.3", + "ngx-guided-tour": "^1.1.10", "rxjs": "^6.3.2", "tinymce": "^5.1.6", "tslib": "^1.10.0", diff --git a/dmp-frontend/src/app/app.component.html b/dmp-frontend/src/app/app.component.html index 6243a45e8..3e7595fa7 100644 --- a/dmp-frontend/src/app/app.component.html +++ b/dmp-frontend/src/app/app.component.html @@ -24,3 +24,5 @@ + + \ No newline at end of file diff --git a/dmp-frontend/src/app/app.module.ts b/dmp-frontend/src/app/app.module.ts index f8575729f..a460a2e19 100644 --- a/dmp-frontend/src/app/app.module.ts +++ b/dmp-frontend/src/app/app.module.ts @@ -32,6 +32,7 @@ import { BaseHttpService } from './core/services/http/base-http.service'; import { ConfigurationService } from './core/services/configuration/configuration.service'; import { Oauth2DialogModule } from './ui/misc/oauth2-dialog/oauth2-dialog.module'; import { MAT_FORM_FIELD_DEFAULT_OPTIONS, MatFormFieldDefaultOptions } from '@angular/material'; +import { GuidedTourModule } from './library/guided-tour/guided-tour.module'; // AoT requires an exported function for factories export function HttpLoaderFactory(http: HttpClient, appConfig: ConfigurationService) { @@ -105,7 +106,8 @@ const appearance: MatFormFieldDefaultOptions = { NavbarModule, SidebarModule, NgcCookieConsentModule.forRoot(cookieConfig), - Oauth2DialogModule + Oauth2DialogModule, + GuidedTourModule.forRoot() ], declarations: [ AppComponent, diff --git a/dmp-frontend/src/app/library/guided-tour/guided-tour-base-theme.scss b/dmp-frontend/src/app/library/guided-tour/guided-tour-base-theme.scss new file mode 100644 index 000000000..bed340850 --- /dev/null +++ b/dmp-frontend/src/app/library/guided-tour/guided-tour-base-theme.scss @@ -0,0 +1,132 @@ +$tour-zindex: 1081 !default; +$tour-step-color: #ffffff !default; +$tour-text-color: #231f1f !default; +$tour-next-button-color: #007bff !default; +$tour-next-button-hover: #0069d9 !default; +$tour-back-button-color: #007bff !default; +$tour-next-text-color: #ffffff !default; +$tour-next-text-hover: #ffffff !default; +$tour-skip-link-color: #5e5e5e !default; +$tour-orb-color: #625aff !default; +$tour-shadow-color: #4c4c4c !default; + +body.tour-open { + overflow: hidden; +} + +@mixin tour-triangle($direction, $color: currentColor, $size: 1rem) { + + @if not index(top right bottom left, $direction) { + @error 'Direction must be either `top`, `right`, `bottom` or `left`.'; + } + + $opposite-direction: top; + + @if $direction==top { + $opposite-direction: bottom; + } + + @if $direction==bottom { + $opposite-direction: top; + } + + @if $direction==right { + $opposite-direction: left; + } + + @if $direction==left { + $opposite-direction: right; + } + + width: 0; + height: 0; + content: ''; + z-index: 2; + border-#{$opposite-direction}: $size solid $color; + $perpendicular-borders: $size solid transparent; + @if $direction==top or $direction==bottom { + border-left: $perpendicular-borders; + border-right: $perpendicular-borders; + } + @else if $direction==right or $direction==left { + border-bottom: $perpendicular-borders; + border-top: $perpendicular-borders; + } +} + +ngx-guided-tour { + .guided-tour-user-input-mask { + z-index: $tour-zindex; + } + + .guided-tour-spotlight-overlay { + z-index: $tour-zindex + 1; + } + + .tour-orb { + z-index: $tour-zindex - 2; + background-color: $tour-orb-color; + box-shadow: 0 0 0.3rem 0.1rem $tour-orb-color; + + .tour-orb-ring { + &::after { + border: 1rem solid $tour-orb-color; + box-shadow: 0 0 0.1rem 0.1rem $tour-orb-color; + } + } + } + + .tour-step { + z-index: $tour-zindex + 2; + + &.tour-bottom, &.tour-bottom-right, &.tour-bottom-left { + .tour-arrow::before { + @include tour-triangle(top, $tour-step-color); + } + } + + &.tour-top, &.tour-top-right, &.tour-top-left { + .tour-arrow::before { + @include tour-triangle(bottom, $tour-step-color); + } + } + + &.tour-left { + .tour-arrow::before { + @include tour-triangle(right, $tour-step-color); + } + } + + &.tour-right { + .tour-arrow::before { + @include tour-triangle(left, $tour-step-color); + } + } + + .tour-block { + color: $tour-text-color; + background-color: $tour-step-color; + box-shadow: 0 0.4rem 0.6rem $tour-shadow-color; + } + + .tour-buttons { + button.skip-button { + color: $tour-skip-link-color; + } + + .back-button { + color: $tour-back-button-color; + } + + .next-button { + background-color: $tour-next-button-color; + color: $tour-next-text-color; + &:hover { + background-color: $tour-next-button-hover; + color: $tour-next-text-hover; + } + } + } + } + +} diff --git a/dmp-frontend/src/app/library/guided-tour/guided-tour.component.html b/dmp-frontend/src/app/library/guided-tour/guided-tour.component.html new file mode 100644 index 000000000..e0c5e0174 --- /dev/null +++ b/dmp-frontend/src/app/library/guided-tour/guided-tour.component.html @@ -0,0 +1,86 @@ + +
+
+
+
+
+
+
+
+
+ +

+ {{currentTourStep.title}} +

+

+ {{ currentTourStep.title }} +

+ +
+ + + + + + + +
+
+
+
+
+ \ No newline at end of file diff --git a/dmp-frontend/src/app/library/guided-tour/guided-tour.component.scss b/dmp-frontend/src/app/library/guided-tour/guided-tour.component.scss new file mode 100644 index 000000000..fa2c91615 --- /dev/null +++ b/dmp-frontend/src/app/library/guided-tour/guided-tour.component.scss @@ -0,0 +1,235 @@ +ngx-guided-tour { + .guided-tour-user-input-mask { + position: fixed; + top: 0; + left: 0; + display: block; + height: 100%; + width: 100%; + max-height: 100vh; + text-align: center; + opacity: 0; + } + + .guided-tour-spotlight-overlay { + position: fixed; + box-shadow: 0 0 0 9999px rgba(0,0,0,.7), 0 0 1.5rem rgba(0,0,0,.5); + border-radius: 44px; + } + + .tour-orb { + position: fixed; + width: 20px; + height: 20px; + border-radius: 50%; + + .tour-orb-ring { + width: 35px; + height: 35px; + position: relative; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + animation: pulse 2s linear infinite; + + &:after { + content: ''; + display: inline-block; + height: 100%; + width: 100%; + border-radius: 50%; + } + } + + @keyframes pulse { + from { + transform: translate(-50%, -50%) scale(0.45); + opacity: 1.0; + } + to { + transform: translate(-50%, -50%) scale(1); + opacity: 0.0; + } + } + } + + .tour-step { + position: fixed; + &.page-tour-step { + max-width: 1043px; + width: 50%; + left: 50%; + top: 50%; + border-radius: 5px; + transform: translate(-50%, -50%) + } + &.tour-bottom, &.tour-bottom-right, &.tour-bottom-left { + .tour-arrow::before { + position: absolute; + } + .tour-block { + margin-top: 10px; + } + } + + &.tour-top, &.tour-top-right, &.tour-top-left { + margin-bottom: 10px; + + .tour-arrow::before { + position: absolute; + bottom: 0; + } + .tour-block { + margin-bottom: 10px; + } + } + + &.tour-bottom , &.tour-top { + .tour-arrow::before { + transform: translateX(-50%); + left: 50%; + } + } + + &.tour-bottom-right, &.tour-top-right { + .tour-arrow::before { + transform: translateX(-100%); + left: calc(100% - 5px); + } + } + + &.tour-bottom-left, &.tour-top-left { + .tour-arrow::before { + left: 5px; + } + } + + &.tour-left { + .tour-arrow::before { + position: absolute; + left: 100%; + transform: translateX(-100%); + top: 5px; + } + .tour-block { + margin-right: 10px; + } + } + + &.tour-right { + .tour-arrow::before { + position: absolute; + left: 0; + top: 5px; + } + .tour-block { + margin-left: 10px; + } + } + + .tour-block { + padding: 15px 25px; + height: 348px; + border-radius: 5px; + } + + .tour-progress-indicator { + padding-bottom: 15px; + } + + .tour-title { + font-weight: lighter !important; + font-size: 16px !important; + padding-left: 65px; + padding-top: 28px; + padding-right: 156px; + // padding-bottom: 20px; + text-align: left; + color: #212121; + line-height: 26px; + white-space:pre-line; + height: 182px; + } + + h3.tour-title { + font-size: 20px; + } + h2.tour-title { + font-size: 30px; + } + + .tour-content { + min-height: 80px; + padding-bottom: 30px; + font-size: 15px; + } + + .tour-buttons { + overflow: hidden; // clearfix + padding: 40px 156px 25px 65px; + + button.link-button { + padding-left: 0; + font-size: 15px; + font-weight: bold; + max-width: none !important; + cursor: pointer; + text-align: center; + white-space: nowrap; + vertical-align: middle; + border: 1px solid transparent; + line-height: 1.5; + background-color: transparent; + position: relative; + outline: none; + padding: 0 15px; + -webkit-appearance: button; + } + + button.skip-button.link-button { + padding: 0; + border-left: 0; + float: right; + width: 133px; + height: 40px; + border: 1px solid #129D99; + background: #FFFFFF 0% 0% no-repeat padding-box; + color: #129D99; + // text-align: center; + } + + .back-button { + float: right; + } + + .next-button { + cursor: pointer; + float: left; + border: none; + outline: none; + padding: 10px 0px;; + width: 101px; + background: #129D99 0% 0% no-repeat padding-box; + } + + button.skip-button.link-button, .next-button { + font-size: 14px; + font-weight: bold; + letter-spacing: 0.35px; + height: 40px; + box-shadow: 0px 3px 6px #1E202029; + border-radius: 30px; + } + } + + .argos-present-img { + background: url("../../../assets/splash/assets/img/argos\ present.png") no-repeat; + width: 176px; + height: 220px; + position: relative; + top: -205px; + left: 820px; + border-top: none; + } + } +} diff --git a/dmp-frontend/src/app/library/guided-tour/guided-tour.component.ts b/dmp-frontend/src/app/library/guided-tour/guided-tour.component.ts new file mode 100644 index 000000000..2fb3ed1bc --- /dev/null +++ b/dmp-frontend/src/app/library/guided-tour/guided-tour.component.ts @@ -0,0 +1,392 @@ +import { AfterViewInit, Component, ElementRef, Input, OnDestroy, ViewChild, ViewEncapsulation, TemplateRef, Inject } from '@angular/core'; +import { fromEvent, Subscription } from 'rxjs'; +import { DOCUMENT } from '@angular/common'; +import { Orientation, TourStep, ProgressIndicatorLocation } from './guided-tour.constants'; +import { GuidedTourService } from './guided-tour.service'; +import { WindowRefService } from "./windowref.service"; + +@Component({ + selector: 'ngx-guided-tour', + templateUrl: './guided-tour.component.html', + styleUrls: ['./guided-tour.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class GuidedTourComponent implements AfterViewInit, OnDestroy { + @Input() public topOfPageAdjustment ?= 0; + @Input() public tourStepWidth ?= 1043; + @Input() public minimalTourStepWidth ?= 900; + @Input() public skipText ?= 'Leave Tour'; + @Input() public nextText ?= 'Got it!'; + @Input() public doneText ?= 'Done'; + @Input() public closeText ?= 'Close'; + @Input() public backText ?= 'Back'; + @Input() public progressIndicatorLocation?: ProgressIndicatorLocation = ProgressIndicatorLocation.InsideNextButton; + @Input() public progressIndicator?: TemplateRef = undefined; + @ViewChild('tourStep', { static: false }) public tourStep: ElementRef; + public highlightPadding = 4; + public currentTourStep: TourStep = null; + public selectedElementRect: DOMRect = null; + public isOrbShowing = false; + public progressIndicatorLocations = ProgressIndicatorLocation; + + private resizeSubscription: Subscription; + private scrollSubscription: Subscription; + + constructor( + public guidedTourService: GuidedTourService, + private windowRef: WindowRefService, + @Inject(DOCUMENT) private dom: any + ) { } + + private get maxWidthAdjustmentForTourStep(): number { + return this.tourStepWidth - this.minimalTourStepWidth; + } + + private get widthAdjustmentForScreenBound(): number { + if (!this.tourStep) { + return 0; + } + let adjustment = 0; + if (this.calculatedLeftPosition < 0) { + adjustment = -this.calculatedLeftPosition; + } + if (this.calculatedLeftPosition > this.windowRef.nativeWindow.innerWidth - this.tourStepWidth) { + adjustment = this.calculatedLeftPosition - (this.windowRef.nativeWindow.innerWidth - this.tourStepWidth); + } + + return Math.min(this.maxWidthAdjustmentForTourStep, adjustment); + } + + public get calculatedTourStepWidth() { + return this.tourStepWidth - this.widthAdjustmentForScreenBound; + } + + public ngAfterViewInit(): void { + this.guidedTourService.guidedTourCurrentStepStream.subscribe((step: TourStep) => { + this.currentTourStep = step; + if (step && step.selector) { + const selectedElement = this.dom.querySelector(step.selector); + if (selectedElement) { + this.handleOrb(); + this.scrollToAndSetElement(); + } else { + this.selectedElementRect = null; + } + } else { + this.selectedElementRect = null; + } + }); + + this.guidedTourService.guidedTourOrbShowingStream.subscribe((value: boolean) => { + this.isOrbShowing = value; + }); + + this.resizeSubscription = fromEvent(this.windowRef.nativeWindow, 'resize').subscribe(() => { + this.updateStepLocation(); + }); + + this.scrollSubscription = fromEvent(this.windowRef.nativeWindow, 'scroll').subscribe(() => { + this.updateStepLocation(); + }); + } + + public ngOnDestroy(): void { + this.resizeSubscription.unsubscribe(); + this.scrollSubscription.unsubscribe(); + } + + public scrollToAndSetElement(): void { + this.updateStepLocation(); + // Allow things to render to scroll to the correct location + setTimeout(() => { + if (!this.isOrbShowing && !this.isTourOnScreen()) { + if (this.selectedElementRect && this.isBottom()) { + // Scroll so the element is on the top of the screen. + const topPos = ((this.windowRef.nativeWindow.scrollY + this.selectedElementRect.top) - this.topOfPageAdjustment) + - (this.currentTourStep.scrollAdjustment ? this.currentTourStep.scrollAdjustment : 0) + + this.getStepScreenAdjustment(); + try { + this.windowRef.nativeWindow.scrollTo({ + left: null, + top: topPos, + behavior: 'smooth' + }); + } catch (err) { + if (err instanceof TypeError) { + this.windowRef.nativeWindow.scroll(0, topPos); + } else { + throw err; + } + } + } else { + // Scroll so the element is on the bottom of the screen. + const topPos = (this.windowRef.nativeWindow.scrollY + this.selectedElementRect.top + this.selectedElementRect.height) + - this.windowRef.nativeWindow.innerHeight + + (this.currentTourStep.scrollAdjustment ? this.currentTourStep.scrollAdjustment : 0) + - this.getStepScreenAdjustment(); + try { + this.windowRef.nativeWindow.scrollTo({ + left: null, + top: topPos, + behavior: 'smooth' + }); + } catch (err) { + if (err instanceof TypeError) { + this.windowRef.nativeWindow.scroll(0, topPos); + } else { + throw err; + } + } + } + } + }); + } + + public handleOrb(): void { + this.guidedTourService.activateOrb(); + if (this.currentTourStep && this.currentTourStep.selector) { + this.scrollToAndSetElement(); + } + } + + private isTourOnScreen(): boolean { + return this.tourStep + && this.elementInViewport(this.dom.querySelector(this.currentTourStep.selector)) + && this.elementInViewport(this.tourStep.nativeElement); + } + + private elementInViewport(element: HTMLElement): boolean { + let top = element.offsetTop; + const height = element.offsetHeight; + + while (element.offsetParent) { + element = (element.offsetParent as HTMLElement); + top += element.offsetTop; + } + if (this.isBottom()) { + return ( + top >= (this.windowRef.nativeWindow.pageYOffset + + this.topOfPageAdjustment + + (this.currentTourStep.scrollAdjustment ? this.currentTourStep.scrollAdjustment : 0) + + this.getStepScreenAdjustment()) + && (top + height) <= (this.windowRef.nativeWindow.pageYOffset + this.windowRef.nativeWindow.innerHeight) + ); + } else { + return ( + top >= (this.windowRef.nativeWindow.pageYOffset + this.topOfPageAdjustment - this.getStepScreenAdjustment()) + && (top + height + (this.currentTourStep.scrollAdjustment ? this.currentTourStep.scrollAdjustment : 0)) <= (this.windowRef.nativeWindow.pageYOffset + this.windowRef.nativeWindow.innerHeight) + ); + } + } + + public backdropClick(event: Event): void { + if (this.guidedTourService.preventBackdropFromAdvancing) { + event.stopPropagation(); + } else { + this.guidedTourService.nextStep(); + } + } + + public updateStepLocation(): void { + if (this.currentTourStep && this.currentTourStep.selector) { + const selectedElement = this.dom.querySelector(this.currentTourStep.selector); + if (selectedElement && typeof selectedElement.getBoundingClientRect === 'function') { + this.selectedElementRect = (selectedElement.getBoundingClientRect() as DOMRect); + } else { + this.selectedElementRect = null; + } + } else { + this.selectedElementRect = null; + } + } + + private isBottom(): boolean { + return this.currentTourStep.orientation + && (this.currentTourStep.orientation === Orientation.Bottom + || this.currentTourStep.orientation === Orientation.BottomLeft + || this.currentTourStep.orientation === Orientation.BottomRight); + } + + public get topPosition(): number { + const paddingAdjustment = this.getHighlightPadding(); + + if (this.isBottom()) { + return this.selectedElementRect.top + this.selectedElementRect.height + paddingAdjustment; + } + + return this.selectedElementRect.top - this.getHighlightPadding(); + } + + public get orbTopPosition(): number { + if (this.isBottom()) { + return this.selectedElementRect.top + this.selectedElementRect.height; + } + + if ( + this.currentTourStep.orientation === Orientation.Right + || this.currentTourStep.orientation === Orientation.Left + ) { + return (this.selectedElementRect.top + (this.selectedElementRect.height / 2)); + } + + return this.selectedElementRect.top; + } + + private get calculatedLeftPosition(): number { + const paddingAdjustment = this.getHighlightPadding(); + + if ( + this.currentTourStep.orientation === Orientation.TopRight + || this.currentTourStep.orientation === Orientation.BottomRight + ) { + return (this.selectedElementRect.right - this.tourStepWidth); + } + + if ( + this.currentTourStep.orientation === Orientation.TopLeft + || this.currentTourStep.orientation === Orientation.BottomLeft + ) { + return (this.selectedElementRect.left); + } + + if (this.currentTourStep.orientation === Orientation.Left) { + return this.selectedElementRect.left - this.tourStepWidth - paddingAdjustment; + } + + if (this.currentTourStep.orientation === Orientation.Right) { + return (this.selectedElementRect.left + this.selectedElementRect.width + paddingAdjustment); + } + + return (this.selectedElementRect.right - (this.selectedElementRect.width / 2) - (this.tourStepWidth / 2)); + } + + public get leftPosition(): number { + if (this.calculatedLeftPosition >= 0) { + return this.calculatedLeftPosition; + } + const adjustment = Math.max(0, -this.calculatedLeftPosition) + const maxAdjustment = Math.min(this.maxWidthAdjustmentForTourStep, adjustment); + return this.calculatedLeftPosition + maxAdjustment; + } + + public get orbLeftPosition(): number { + if ( + this.currentTourStep.orientation === Orientation.TopRight + || this.currentTourStep.orientation === Orientation.BottomRight + ) { + return this.selectedElementRect.right; + } + + if ( + this.currentTourStep.orientation === Orientation.TopLeft + || this.currentTourStep.orientation === Orientation.BottomLeft + ) { + return this.selectedElementRect.left; + } + + if (this.currentTourStep.orientation === Orientation.Left) { + return this.selectedElementRect.left; + } + + if (this.currentTourStep.orientation === Orientation.Right) { + return (this.selectedElementRect.left + this.selectedElementRect.width); + } + + return (this.selectedElementRect.right - (this.selectedElementRect.width / 2)); + } + + public get transform(): string { + if ( + !this.currentTourStep.orientation + || this.currentTourStep.orientation === Orientation.Top + || this.currentTourStep.orientation === Orientation.TopRight + || this.currentTourStep.orientation === Orientation.TopLeft + ) { + return 'translateY(-100%)'; + } + return null; + } + + public get orbTransform(): string { + if ( + !this.currentTourStep.orientation + || this.currentTourStep.orientation === Orientation.Top + || this.currentTourStep.orientation === Orientation.Bottom + || this.currentTourStep.orientation === Orientation.TopLeft + || this.currentTourStep.orientation === Orientation.BottomLeft + ) { + return 'translateY(-50%)'; + } + + if ( + this.currentTourStep.orientation === Orientation.TopRight + || this.currentTourStep.orientation === Orientation.BottomRight + ) { + return 'translate(-100%, -50%)'; + } + + if ( + this.currentTourStep.orientation === Orientation.Right + || this.currentTourStep.orientation === Orientation.Left + ) { + return 'translate(-50%, -50%)'; + } + + return null; + } + + public get overlayTop(): number { + if (this.selectedElementRect) { + return this.selectedElementRect.top - this.getHighlightPadding(); + } + return 0; + } + + public get overlayLeft(): number { + if (this.selectedElementRect) { + return this.selectedElementRect.left - this.getHighlightPadding(); + } + return 0; + } + + public get overlayHeight(): number { + if (this.selectedElementRect) { + return this.selectedElementRect.height + (this.getHighlightPadding() * 2); + } + return 0; + } + + public get overlayWidth(): number { + if (this.selectedElementRect) { + return (this.selectedElementRect.width + (this.getHighlightPadding() * 2)) * 0.95; + } + return 0; + } + + private getHighlightPadding(): number { + let paddingAdjustment = this.currentTourStep.useHighlightPadding ? this.highlightPadding : 0; + if (this.currentTourStep.highlightPadding) { + paddingAdjustment = this.currentTourStep.highlightPadding; + } + return paddingAdjustment; + } + + // This calculates a value to add or subtract so the step should not be off screen. + private getStepScreenAdjustment(): number { + if ( + this.currentTourStep.orientation === Orientation.Left + || this.currentTourStep.orientation === Orientation.Right + ) { + return 0; + } + + const scrollAdjustment = this.currentTourStep.scrollAdjustment ? this.currentTourStep.scrollAdjustment : 0; + const tourStepHeight = typeof this.tourStep.nativeElement.getBoundingClientRect === 'function' ? this.tourStep.nativeElement.getBoundingClientRect().height : 0; + const elementHeight = this.selectedElementRect.height + scrollAdjustment + tourStepHeight; + + if ((this.windowRef.nativeWindow.innerHeight - this.topOfPageAdjustment) < elementHeight) { + return elementHeight - (this.windowRef.nativeWindow.innerHeight - this.topOfPageAdjustment); + } + return 0; + } +} diff --git a/dmp-frontend/src/app/library/guided-tour/guided-tour.constants.ts b/dmp-frontend/src/app/library/guided-tour/guided-tour.constants.ts new file mode 100644 index 000000000..6053c2583 --- /dev/null +++ b/dmp-frontend/src/app/library/guided-tour/guided-tour.constants.ts @@ -0,0 +1,75 @@ + +export interface TourStep { + /** Selector for element that will be highlighted */ + selector?: string; + /** Tour title text */ + title?: string; + /** Tour step text */ + content: string; + /** Where the tour step will appear next to the selected element */ + orientation?: Orientation | OrientationConfiguration[]; + /** Action that happens when the step is opened */ + action?: () => void; + /** Action that happens when the step is closed */ + closeAction?: () => void; + /** Skips this step, this is so you do not have create multiple tour configurations based on user settings/configuration */ + skipStep?: boolean; + /** Adds some padding for things like sticky headers when scrolling to an element */ + scrollAdjustment?: number; + /** Adds default padding around tour highlighting. Does not need to be true for highlightPadding to work */ + useHighlightPadding?: boolean; + /** Adds padding around tour highlighting in pixels, this overwrites the default for this step. Is not dependent on useHighlightPadding being true */ + highlightPadding?: number; +} + +export interface GuidedTour { + /** Identifier for tour */ + tourId: string; + /** Use orb to start tour */ + useOrb?: boolean; + /** Steps fo the tour */ + steps: TourStep[]; + /** Function will be called when tour is skipped */ + skipCallback?: (stepSkippedOn: number) => void; + /** Function will be called when tour is completed */ + completeCallback?: () => void; + /** Minimum size of screen in pixels before the tour is run, if the tour is resized below this value the user will be told to resize */ + minimumScreenSize?: number; + /** Dialog shown if the window width is smaller than the defined minimum screen size. */ + resizeDialog?: { + /** Resize dialog title text */ + title?: string; + /** Resize dialog text */ + content: string; + } + /** + * Prevents the tour from advancing by clicking the backdrop. + * This should only be set if you are completely sure your tour is displaying correctly on all screen sizes otherwise a user can get stuck. + */ + preventBackdropFromAdvancing?: boolean; +} + +export interface OrientationConfiguration { + /** Where the tour step will appear next to the selected element */ + orientationDirection: Orientation; + /** When this orientation configuration starts in pixels */ + maximumSize?: number; +} + +export class Orientation { + public static readonly Bottom = 'bottom'; + public static readonly BottomLeft = 'bottom-left'; + public static readonly BottomRight = 'bottom-right'; + public static readonly Center = 'center'; + public static readonly Left = 'left'; + public static readonly Right = 'right'; + public static readonly Top = 'top'; + public static readonly TopLeft = 'top-left'; + public static readonly TopRight = 'top-right'; +} + +export enum ProgressIndicatorLocation { + InsideNextButton = 'inside-next-button', + TopOfTourBlock = 'top-of-tour-block', + None = 'none', +} diff --git a/dmp-frontend/src/app/library/guided-tour/guided-tour.module.ts b/dmp-frontend/src/app/library/guided-tour/guided-tour.module.ts new file mode 100644 index 000000000..d51266431 --- /dev/null +++ b/dmp-frontend/src/app/library/guided-tour/guided-tour.module.ts @@ -0,0 +1,21 @@ +import { GuidedTourService } from './guided-tour.service'; +import { GuidedTourComponent } from './guided-tour.component'; +import { NgModule, ErrorHandler, ModuleWithProviders } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { WindowRefService } from './windowref.service'; + +@NgModule({ + declarations: [GuidedTourComponent], + imports: [CommonModule], + providers: [WindowRefService], + exports: [GuidedTourComponent], + entryComponents: [GuidedTourComponent], +}) +export class GuidedTourModule { + public static forRoot(): ModuleWithProviders { + return { + ngModule: GuidedTourModule, + providers: [ErrorHandler, GuidedTourService], + }; + } +} diff --git a/dmp-frontend/src/app/library/guided-tour/guided-tour.service.ts b/dmp-frontend/src/app/library/guided-tour/guided-tour.service.ts new file mode 100644 index 000000000..2823f24d0 --- /dev/null +++ b/dmp-frontend/src/app/library/guided-tour/guided-tour.service.ts @@ -0,0 +1,229 @@ +import { debounceTime } from 'rxjs/operators'; +import { ErrorHandler, Inject, Injectable } from '@angular/core'; +import { Observable, Subject, fromEvent } from 'rxjs'; +import { GuidedTour, TourStep, Orientation, OrientationConfiguration } from './guided-tour.constants'; +import { cloneDeep } from 'lodash'; +import { DOCUMENT } from "@angular/common"; +import { WindowRefService } from "./windowref.service"; + +@Injectable() +export class GuidedTourService { + public guidedTourCurrentStepStream: Observable; + public guidedTourOrbShowingStream: Observable; + + private _guidedTourCurrentStepSubject = new Subject(); + private _guidedTourOrbShowingSubject = new Subject(); + private _currentTourStepIndex = 0; + private _currentTour: GuidedTour = null; + private _onFirstStep = true; + private _onLastStep = true; + private _onResizeMessage = false; + + constructor( + public errorHandler: ErrorHandler, + private windowRef: WindowRefService, + @Inject(DOCUMENT) private dom + ) { + this.guidedTourCurrentStepStream = this._guidedTourCurrentStepSubject.asObservable(); + this.guidedTourOrbShowingStream = this._guidedTourOrbShowingSubject.asObservable(); + + fromEvent(this.windowRef.nativeWindow, 'resize').pipe(debounceTime(200)).subscribe(() => { + if (this._currentTour && this._currentTourStepIndex > -1) { + if (this._currentTour.minimumScreenSize && this._currentTour.minimumScreenSize >= this.windowRef.nativeWindow.innerWidth) { + this._onResizeMessage = true; + const dialog = this._currentTour.resizeDialog || { + title: 'Please resize', + content: 'You have resized the tour to a size that is too small to continue. Please resize the browser to a larger size to continue the tour or close the tour.' + }; + + this._guidedTourCurrentStepSubject.next(dialog); + } else { + this._onResizeMessage = false; + this._guidedTourCurrentStepSubject.next(this.getPreparedTourStep(this._currentTourStepIndex)); + } + } + }); + } + + public nextStep(): void { + if (this._currentTour.steps[this._currentTourStepIndex].closeAction) { + this._currentTour.steps[this._currentTourStepIndex].closeAction(); + } + if (this._currentTour.steps[this._currentTourStepIndex + 1]) { + this._currentTourStepIndex++; + this._setFirstAndLast(); + if (this._currentTour.steps[this._currentTourStepIndex].action) { + this._currentTour.steps[this._currentTourStepIndex].action(); + // Usually an action is opening something so we need to give it time to render. + setTimeout(() => { + if (this._checkSelectorValidity()) { + this._guidedTourCurrentStepSubject.next(this.getPreparedTourStep(this._currentTourStepIndex)); + } else { + this.nextStep(); + } + }); + } else { + if (this._checkSelectorValidity()) { + this._guidedTourCurrentStepSubject.next(this.getPreparedTourStep(this._currentTourStepIndex)); + } else { + this.nextStep(); + } + } + } else { + if (this._currentTour.completeCallback) { + this._currentTour.completeCallback(); + } + this.resetTour(); + } + } + + public backStep(): void { + if (this._currentTour.steps[this._currentTourStepIndex].closeAction) { + this._currentTour.steps[this._currentTourStepIndex].closeAction(); + } + if (this._currentTour.steps[this._currentTourStepIndex - 1]) { + this._currentTourStepIndex--; + this._setFirstAndLast(); + if (this._currentTour.steps[this._currentTourStepIndex].action) { + this._currentTour.steps[this._currentTourStepIndex].action(); + setTimeout(() => { + if (this._checkSelectorValidity()) { + this._guidedTourCurrentStepSubject.next(this.getPreparedTourStep(this._currentTourStepIndex)); + } else { + this.backStep(); + } + }); + } else { + if (this._checkSelectorValidity()) { + this._guidedTourCurrentStepSubject.next(this.getPreparedTourStep(this._currentTourStepIndex)); + } else { + this.backStep(); + } + } + } else { + this.resetTour(); + } + } + + public skipTour(): void { + if (this._currentTour.skipCallback) { + this._currentTour.skipCallback(this._currentTourStepIndex); + } + this.resetTour(); + } + + public resetTour(): void { + this.dom.body.classList.remove('tour-open'); + this._currentTour = null; + this._currentTourStepIndex = 0; + this._guidedTourCurrentStepSubject.next(null); + } + + public startTour(tour: GuidedTour): void { + this._currentTour = cloneDeep(tour); + this._currentTour.steps = this._currentTour.steps.filter(step => !step.skipStep); + this._currentTourStepIndex = 0; + this._setFirstAndLast(); + this._guidedTourOrbShowingSubject.next(this._currentTour.useOrb); + if ( + this._currentTour.steps.length > 0 + && (!this._currentTour.minimumScreenSize + || (this.windowRef.nativeWindow.innerWidth >= this._currentTour.minimumScreenSize)) + ) { + if (!this._currentTour.useOrb) { + this.dom.body.classList.add('tour-open'); + } + if (this._currentTour.steps[this._currentTourStepIndex].action) { + this._currentTour.steps[this._currentTourStepIndex].action(); + } + if (this._checkSelectorValidity()) { + this._guidedTourCurrentStepSubject.next(this.getPreparedTourStep(this._currentTourStepIndex)); + } else { + this.nextStep(); + } + } + } + + public activateOrb(): void { + this._guidedTourOrbShowingSubject.next(false); + this.dom.body.classList.add('tour-open'); + } + + private _setFirstAndLast(): void { + this._onLastStep = (this._currentTour.steps.length - 1) === this._currentTourStepIndex; + this._onFirstStep = this._currentTourStepIndex === 0; + } + + private _checkSelectorValidity(): boolean { + if (this._currentTour.steps[this._currentTourStepIndex].selector) { + const selectedElement = this.dom.querySelector(this._currentTour.steps[this._currentTourStepIndex].selector); + if (!selectedElement) { + this.errorHandler.handleError( + // If error handler is configured this should not block the browser. + new Error(`Error finding selector ${this._currentTour.steps[this._currentTourStepIndex].selector} on step ${this._currentTourStepIndex + 1} during guided tour: ${this._currentTour.tourId}`) + ); + return false; + } + } + return true; + } + + public get onLastStep(): boolean { + return this._onLastStep; + } + + public get onFirstStep(): boolean { + return this._onFirstStep; + } + + public get onResizeMessage(): boolean { + return this._onResizeMessage; + } + + public get currentTourStepDisplay(): number { + return this._currentTourStepIndex + 1; + } + + public get currentTourStepCount(): number { + return this._currentTour && this._currentTour.steps ? this._currentTour.steps.length : 0; + } + + public get preventBackdropFromAdvancing(): boolean { + return this._currentTour && this._currentTour.preventBackdropFromAdvancing; + } + + private getPreparedTourStep(index: number): TourStep { + return this.setTourOrientation(this._currentTour.steps[index]); + } + + private setTourOrientation(step: TourStep): TourStep { + const convertedStep = cloneDeep(step); + if ( + convertedStep.orientation + && !(typeof convertedStep.orientation === 'string') + && (convertedStep.orientation as OrientationConfiguration[]).length + ) { + (convertedStep.orientation as OrientationConfiguration[]).sort((a: OrientationConfiguration, b: OrientationConfiguration) => { + if (!b.maximumSize) { + return 1; + } + if (!a.maximumSize) { + return -1; + } + return b.maximumSize - a.maximumSize; + }); + + let currentOrientation: Orientation = Orientation.Top; + (convertedStep.orientation as OrientationConfiguration[]).forEach( + (orientationConfig: OrientationConfiguration) => { + if (!orientationConfig.maximumSize || this.windowRef.nativeWindow.innerWidth <= orientationConfig.maximumSize) { + currentOrientation = orientationConfig.orientationDirection; + } + } + ); + + convertedStep.orientation = currentOrientation; + } + return convertedStep; + } +} diff --git a/dmp-frontend/src/app/library/guided-tour/windowref.service.ts b/dmp-frontend/src/app/library/guided-tour/windowref.service.ts new file mode 100644 index 000000000..f5ef09652 --- /dev/null +++ b/dmp-frontend/src/app/library/guided-tour/windowref.service.ts @@ -0,0 +1,38 @@ +import { Inject, Injectable, PLATFORM_ID } from "@angular/core"; +import { isPlatformBrowser } from "@angular/common"; + +function getWindow(): any { + return window; +} + +function getMockWindow(): any { + return { + innerWidth: 0, + innerHeight: 0, + scrollY: 0, + scrollX: 0, + pageYOffset: 0, + pageXOffset: 0, + scroll: () => {}, + scrollTo: () => {}, + addEventListener: () => {}, + removeEventListener: () => {}, + } +} + +@Injectable() +export class WindowRefService { + private readonly isBrowser: boolean = false; + + get nativeWindow(): any { + if (this.isBrowser) { + return getWindow(); + } else { + return getMockWindow(); + } + } + + constructor(@Inject(PLATFORM_ID) platformId) { + this.isBrowser = isPlatformBrowser(platformId); + } +} diff --git a/dmp-frontend/src/app/ui/dataset/listing/dataset-listing.component.html b/dmp-frontend/src/app/ui/dataset/listing/dataset-listing.component.html index 473a0d08e..1977993fb 100644 --- a/dmp-frontend/src/app/ui/dataset/listing/dataset-listing.component.html +++ b/dmp-frontend/src/app/ui/dataset/listing/dataset-listing.component.html @@ -34,6 +34,11 @@ + +
+ {{ 'GENERAL.ACTIONS.TAKE-A-TOUR'| translate }} +
+ search diff --git a/dmp-frontend/src/app/ui/dataset/listing/dataset-listing.component.scss b/dmp-frontend/src/app/ui/dataset/listing/dataset-listing.component.scss index 656256f4d..0f3d9beb9 100644 --- a/dmp-frontend/src/app/ui/dataset/listing/dataset-listing.component.scss +++ b/dmp-frontend/src/app/ui/dataset/listing/dataset-listing.component.scss @@ -137,6 +137,19 @@ height: 45px; } +.center-content { + width: 100%; + min-width: 10rem; + margin: auto; + padding: 0 15px; + text-align: right; + font-size: 0.875rem; + font-weight: 600; + letter-spacing: 0.02rem; + color: #2D72D6; + cursor: pointer; +} + .search-form { // font-size: 12px; text-align: left; diff --git a/dmp-frontend/src/app/ui/dataset/listing/dataset-listing.component.ts b/dmp-frontend/src/app/ui/dataset/listing/dataset-listing.component.ts index 6122b3d80..a724aba04 100644 --- a/dmp-frontend/src/app/ui/dataset/listing/dataset-listing.component.ts +++ b/dmp-frontend/src/app/ui/dataset/listing/dataset-listing.component.ts @@ -24,6 +24,8 @@ import { MatDialog } from '@angular/material'; import { FormGroup, FormBuilder, FormControl } from '@angular/forms'; import { RecentActivityOrder } from '@app/core/common/enum/recent-activity-order'; import { EnumUtils } from '@app/core/services/utilities/enum-utils.service'; +import { GuidedTourService } from '@app/library/guided-tour/guided-tour.service'; +import { GuidedTour, Orientation } from '@app/library/guided-tour/guided-tour.constants'; @Component({ selector: 'app-dataset-listing-component', @@ -59,6 +61,8 @@ export class DatasetListingComponent extends BaseComponent implements OnInit, IB scrollbar: boolean; order = RecentActivityOrder; + dmpText: string; + datasetText: string; constructor( private datasetService: DatasetService, @@ -68,7 +72,9 @@ export class DatasetListingComponent extends BaseComponent implements OnInit, IB private dmpService: DmpService, private language: TranslateService, private authService: AuthService, - public enumUtils: EnumUtils + public enumUtils: EnumUtils, + private authentication: AuthService, + private guidedTourService: GuidedTourService ) { super(); } @@ -133,6 +139,27 @@ export class DatasetListingComponent extends BaseComponent implements OnInit, IB this.scrollbar = this.hasScrollbar(); } + public dashboardTour: GuidedTour = { + tourId: 'dmp-dataset-tour', + useOrb: true, + steps: [ + { + selector: '.dmp-tour', + content: 'Step 1', + orientation: Orientation.Right + }, + { + selector: '.dataset-tour', + content: 'Step 2', + orientation: Orientation.Right + } + ] + }; + + public isAuthenticated(): boolean { + return !(!this.authentication.current()); + } + controlModified(): void { // this.clearErrorModel(); // if (this.refreshCallback != null && @@ -327,6 +354,28 @@ export class DatasetListingComponent extends BaseComponent implements OnInit, IB return merged; } + public setDashboardTourDmpText(): void { + this.dmpText = this.language.instant('DMP-LISTING.TEXT-INFO') + '\n\n' + + this.language.instant('DMP-LISTING.TEXT-INFO-QUESTION') + ' ' + + this.language.instant('DMP-LISTING.LINK-ZENODO') + ' ' + + this.language.instant('DMP-LISTING.GET-IDEA'); + this.dashboardTour.steps[0].title = this.dmpText; + } + + public setDashboardTourDatasetText(): void { + this.datasetText = this.language.instant('DATASET-LISTING.TEXT-INFO') + + this.language.instant('DATASET-LISTING.LINK-PUBLIC-DATASETS') + ' ' + + this.language.instant('DATASET-LISTING.TEXT-INFO-REST') + '\n\n' + + this.language.instant('DATASET-LISTING.TEXT-INFO-PAR'); + this.dashboardTour.steps[1].title = this.datasetText; + } + + public restartTour(): void { + this.setDashboardTourDmpText(); + this.setDashboardTourDatasetText(); + this.guidedTourService.startTour(this.dashboardTour); + } + // rowClicked(dataset: DatasetListingModel) { // this.router.navigate(['/datasets/edit/' + dataset.id]); // } diff --git a/dmp-frontend/src/app/ui/dmp/listing/dmp-listing.component.html b/dmp-frontend/src/app/ui/dmp/listing/dmp-listing.component.html index 06fed8dee..1f206f8cc 100644 --- a/dmp-frontend/src/app/ui/dmp/listing/dmp-listing.component.html +++ b/dmp-frontend/src/app/ui/dmp/listing/dmp-listing.component.html @@ -29,6 +29,11 @@ + +
+ {{ 'GENERAL.ACTIONS.TAKE-A-TOUR'| translate }} +
+ search diff --git a/dmp-frontend/src/app/ui/dmp/listing/dmp-listing.component.scss b/dmp-frontend/src/app/ui/dmp/listing/dmp-listing.component.scss index d3541c7dc..ba7fc9fc9 100644 --- a/dmp-frontend/src/app/ui/dmp/listing/dmp-listing.component.scss +++ b/dmp-frontend/src/app/ui/dmp/listing/dmp-listing.component.scss @@ -241,6 +241,19 @@ padding: 0.3rem 0rem 0.6rem 0rem !important; } +.center-content { + width: 100%; + min-width: 10rem; + margin: auto; + padding: 0 15px; + text-align: right; + font-size: 0.875rem; + font-weight: 600; + letter-spacing: 0.02rem; + color: #2D72D6; + cursor: pointer; +} + // .bot-paginator { // margin-top: auto; // } diff --git a/dmp-frontend/src/app/ui/dmp/listing/dmp-listing.component.ts b/dmp-frontend/src/app/ui/dmp/listing/dmp-listing.component.ts index b47e12297..aaa8d5f44 100644 --- a/dmp-frontend/src/app/ui/dmp/listing/dmp-listing.component.ts +++ b/dmp-frontend/src/app/ui/dmp/listing/dmp-listing.component.ts @@ -26,7 +26,8 @@ import { AuthService } from '@app/core/services/auth/auth.service'; import { FormBuilder, FormControl, FormGroup } from '@angular/forms'; import { DmpCriteriaDialogComponent } from './criteria/dmp-criteria-dialog.component'; import { RecentActivityOrder } from '@app/core/common/enum/recent-activity-order'; - +import { GuidedTourService } from '@app/library/guided-tour/guided-tour.service'; +import { GuidedTour, Orientation } from '@app/library/guided-tour/guided-tour.constants'; @Component({ selector: 'app-dmp-listing-component', @@ -63,6 +64,8 @@ export class DmpListingComponent extends BaseComponent implements OnInit, IBread scrollbar: boolean; order = RecentActivityOrder; + dmpText: string; + datasetText: string; constructor( private dmpService: DmpService, @@ -73,7 +76,8 @@ export class DmpListingComponent extends BaseComponent implements OnInit, IBread private language: TranslateService, private grantService: GrantService, private uiNotificationService: UiNotificationService, - private authService: AuthService + private authService: AuthService, + private guidedTourService: GuidedTourService ) { super(); } @@ -173,6 +177,26 @@ export class DmpListingComponent extends BaseComponent implements OnInit, IBread .subscribe(x => this.refresh()); } + public dashboardTour: GuidedTour = { + tourId: 'dmp-dataset-tour', + useOrb: true, + steps: [ + { + selector: '.dmp-tour', + content: 'Step 1', + orientation: Orientation.Right + }, + { + selector: '.dataset-tour', + content: 'Step 2', + orientation: Orientation.Right + } + ] + }; + + public isAuthenticated(): boolean { + return !(!this.authService.current()); + } ngAfterContentChecked(): void { this.scrollbar = this.hasScrollbar(); } @@ -426,6 +450,28 @@ export class DmpListingComponent extends BaseComponent implements OnInit, IBread } return merged; } + + public setDashboardTourDmpText(): void { + this.dmpText = this.language.instant('DMP-LISTING.TEXT-INFO') + '\n\n' + + this.language.instant('DMP-LISTING.TEXT-INFO-QUESTION') + ' ' + + this.language.instant('DMP-LISTING.LINK-ZENODO') + ' ' + + this.language.instant('DMP-LISTING.GET-IDEA'); + this.dashboardTour.steps[0].title = this.dmpText; + } + + public setDashboardTourDatasetText(): void { + this.datasetText = this.language.instant('DATASET-LISTING.TEXT-INFO') + + this.language.instant('DATASET-LISTING.LINK-PUBLIC-DATASETS') + ' ' + + this.language.instant('DATASET-LISTING.TEXT-INFO-REST') + '\n\n' + + this.language.instant('DATASET-LISTING.TEXT-INFO-PAR'); + this.dashboardTour.steps[1].title = this.datasetText; + } + + public restartTour(): void { + this.setDashboardTourDmpText(); + this.setDashboardTourDatasetText(); + this.guidedTourService.startTour(this.dashboardTour); + } } // export class DmpDataSource extends DataSource { diff --git a/dmp-frontend/src/app/ui/sidebar/sidebar.component.html b/dmp-frontend/src/app/ui/sidebar/sidebar.component.html index e6a7767dc..4c8ec0e74 100644 --- a/dmp-frontend/src/app/ui/sidebar/sidebar.component.html +++ b/dmp-frontend/src/app/ui/sidebar/sidebar.component.html @@ -5,7 +5,7 @@
- + {{ groupMenuRoute.icon }} person {{groupMenuRoute.title | translate}} diff --git a/dmp-frontend/src/assets/i18n/de.json b/dmp-frontend/src/assets/i18n/de.json index 30401d5d8..ac72f08db 100644 --- a/dmp-frontend/src/assets/i18n/de.json +++ b/dmp-frontend/src/assets/i18n/de.json @@ -79,7 +79,8 @@ "VIEW-ALL": "Alles anzeigen", "SHOW-MORE": "Mehr anzeigen", "SHOW-LESS": "Weniger anzeigen", - "LOG-IN": "Einloggen" + "LOG-IN": "Einloggen", + "TAKE-A-TOUR": "Do you need help? Take a tour.." }, "PREPOSITIONS": { "OF": "von" diff --git a/dmp-frontend/src/assets/i18n/en.json b/dmp-frontend/src/assets/i18n/en.json index 279c79908..d21768178 100644 --- a/dmp-frontend/src/assets/i18n/en.json +++ b/dmp-frontend/src/assets/i18n/en.json @@ -81,7 +81,8 @@ "SHOW-MORE": "Show more", "LOAD-MORE": "Load more", "SHOW-LESS": "Show less", - "LOG-IN": "Log in" + "LOG-IN": "Log in", + "TAKE-A-TOUR": "Do you need help? Take a tour.." }, "PREPOSITIONS": { "OF": "of" diff --git a/dmp-frontend/src/assets/i18n/es.json b/dmp-frontend/src/assets/i18n/es.json index 9b14419b9..87abca842 100644 --- a/dmp-frontend/src/assets/i18n/es.json +++ b/dmp-frontend/src/assets/i18n/es.json @@ -80,7 +80,8 @@ "VIEW-ALL": "Ver todo", "SHOW-MORE": "Mostrar más", "SHOW-LESS": "Show less", - "LOG-IN": "Iniciar sesión" + "LOG-IN": "Iniciar sesión", + "TAKE-A-TOUR": "Do you need help? Take a tour.." }, "PREPOSITIONS": { "OF": "de" diff --git a/dmp-frontend/src/assets/i18n/gr.json b/dmp-frontend/src/assets/i18n/gr.json index c0a7c8ab1..ef03e38fc 100644 --- a/dmp-frontend/src/assets/i18n/gr.json +++ b/dmp-frontend/src/assets/i18n/gr.json @@ -80,7 +80,8 @@ "VIEW-ALL": "Προβολή όλων", "SHOW-MORE": "Δείτε περισσότερα", "SHOW-LESS": "Δείτε λιγότερα", - "LOG-IN": "Σύνδεση" + "LOG-IN": "Σύνδεση", + "TAKE-A-TOUR": "Do you need help? Take a tour.." }, "PREPOSITIONS": { "OF": "of" @@ -1239,9 +1240,7 @@ "SEARCH": "ΑΝΑΖΗΤΗΣΗ...", "DATA-MANAGEMENT-PLANS": "ΣΧΕΔΙΑ ΔΙΑΧΕΙΡΙΣΗΣ ΔΕΔΟΜΕΝΩΝ", "PERSONAL-USAGE": "Προσωπική Χρήση", - "DMPS": "DMPs", "DATASET-DESCRIPTIONS": "Περιγραφές Dataset", - "GRANTS": "Grants", "RELATED-ORGANISATIONS": "Σχετικοί Οργανισμοί", "DRAFTS": "Προσχέδια", "ALL": "Όλα", diff --git a/dmp-frontend/src/assets/i18n/tr.json b/dmp-frontend/src/assets/i18n/tr.json index a5ac3b21a..cf3ef2acf 100644 --- a/dmp-frontend/src/assets/i18n/tr.json +++ b/dmp-frontend/src/assets/i18n/tr.json @@ -79,7 +79,8 @@ "VIEW-ALL": "Tümüne Gör", "SHOW-MORE": "Daha fazla göster", "SHOW-LESS": "Daha az göster", - "LOG-IN": "Oturum aç" + "LOG-IN": "Oturum aç", + "TAKE-A-TOUR": "Do you need help? Take a tour.." }, "PREPOSITIONS": { "OF": "nın" diff --git a/dmp-frontend/src/assets/splash/assets/img/argos present.png b/dmp-frontend/src/assets/splash/assets/img/argos present.png new file mode 100644 index 000000000..9e167acc3 Binary files /dev/null and b/dmp-frontend/src/assets/splash/assets/img/argos present.png differ diff --git a/dmp-frontend/src/assets/splash/assets/img/argos present@2x.png b/dmp-frontend/src/assets/splash/assets/img/argos present@2x.png new file mode 100644 index 000000000..1b99808ea Binary files /dev/null and b/dmp-frontend/src/assets/splash/assets/img/argos present@2x.png differ