diff --git a/apps/client/src/app/pages/landing/landing-page.html b/apps/client/src/app/pages/landing/landing-page.html index bdda5d5b6..96d5c4f7a 100644 --- a/apps/client/src/app/pages/landing/landing-page.html +++ b/apps/client/src/app/pages/landing/landing-page.html @@ -320,49 +320,37 @@
-

+

What our users are saying

- - -
- -
- — - {{ testimonial.author }} - {{ testimonial.author }}, {{ - testimonial.country }} -
-
-
+ +
diff --git a/apps/client/src/app/pages/landing/landing-page.module.ts b/apps/client/src/app/pages/landing/landing-page.module.ts index 6bc321349..4357f52fc 100644 --- a/apps/client/src/app/pages/landing/landing-page.module.ts +++ b/apps/client/src/app/pages/landing/landing-page.module.ts @@ -4,18 +4,18 @@ import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { RouterModule } from '@angular/router'; import { GfWorldMapChartModule } from '@ghostfolio/client/components/world-map-chart/world-map-chart.module'; +import { GfCarouselModule } from '@ghostfolio/ui/carousel'; import { GfLogoModule } from '@ghostfolio/ui/logo'; import { GfValueModule } from '@ghostfolio/ui/value'; -import { CarouselItem } from '@ghostfolio/ui/carousel/carousel-item.directive'; -import { Carousel } from '@ghostfolio/ui/carousel/carousel'; import { LandingPageRoutingModule } from './landing-page-routing.module'; import { LandingPageComponent } from './landing-page.component'; @NgModule({ - declarations: [Carousel, CarouselItem, LandingPageComponent], + declarations: [LandingPageComponent], imports: [ CommonModule, + GfCarouselModule, GfLogoModule, GfValueModule, GfWorldMapChartModule, diff --git a/libs/ui/src/lib/carousel/_carousel-theme.scss. b/libs/ui/src/lib/carousel/_carousel-theme.scss. deleted file mode 100644 index 4c28229a4..000000000 --- a/libs/ui/src/lib/carousel/_carousel-theme.scss. +++ /dev/null @@ -1,16 +0,0 @@ -@use 'sass:map'; -@use '@angular/material' as mat; - -@mixin theme($theme) { - $primary: map.get($theme, primary); - $accent: map.get($theme, accent); - $warn: map.get($theme, warn); - $background: map.get($theme, background); - $foreground: map.get($theme, foreground); - - .docs-carousel-nav.mat-mdc-mini-fab { - background: mat.get-color-from-palette($background, background); - color: mat.get-color-from-palette($foreground, secondary-text) - } - -} \ No newline at end of file diff --git a/libs/ui/src/lib/carousel/carousel-item.directive.ts b/libs/ui/src/lib/carousel/carousel-item.directive.ts index f137c5bf7..95fefe5cc 100644 --- a/libs/ui/src/lib/carousel/carousel-item.directive.ts +++ b/libs/ui/src/lib/carousel/carousel-item.directive.ts @@ -1,14 +1,16 @@ -import { FocusableOption } from "@angular/cdk/a11y"; -import { ElementRef, HostBinding } from "@angular/core"; +import { FocusableOption } from '@angular/cdk/a11y'; +import { Directive, ElementRef, HostBinding } from '@angular/core'; +@Directive({ + selector: '[gf-carousel-item]' +}) +export class CarouselItem implements FocusableOption { + @HostBinding('attr.role') readonly role = 'listitem'; + @HostBinding('tabindex') tabindex = '-1'; - export class CarouselItem implements FocusableOption { - @HostBinding('attr.role') readonly role = 'listitem'; - @HostBinding('tabindex') tabindex = '-1'; - - constructor(readonly element: ElementRef) {} - - focus(): void { - this.element.nativeElement.focus({preventScroll: true}); - } - } \ No newline at end of file + public constructor(readonly element: ElementRef) {} + + public focus(): void { + this.element.nativeElement.focus({ preventScroll: true }); + } +} diff --git a/libs/ui/src/lib/carousel/carousel.component.html b/libs/ui/src/lib/carousel/carousel.component.html new file mode 100644 index 000000000..59966b8a6 --- /dev/null +++ b/libs/ui/src/lib/carousel/carousel.component.html @@ -0,0 +1,34 @@ + + +
+ +
+ + diff --git a/libs/ui/src/lib/carousel/carousel.component.scss b/libs/ui/src/lib/carousel/carousel.component.scss new file mode 100644 index 000000000..38da7c100 --- /dev/null +++ b/libs/ui/src/lib/carousel/carousel.component.scss @@ -0,0 +1,34 @@ +:host { + display: block; + position: relative; + + ::ng-deep { + [gf-carousel-item] { + flex-shrink: 0; + width: 100%; + } + } + + button { + top: 50%; + transform: translateY(-50%); + + &.carousel-nav-prev { + left: -50px; + } + + &.carousel-nav-next { + right: -50px; + } + } + + .carousel-content { + flex-direction: row; + outline: none; + transition: transform 0.5s ease-in-out; + + .animations-disabled & { + transition: none; + } + } +} diff --git a/libs/ui/src/lib/carousel/carousel.component.ts b/libs/ui/src/lib/carousel/carousel.component.ts new file mode 100644 index 000000000..a0eb0f8a1 --- /dev/null +++ b/libs/ui/src/lib/carousel/carousel.component.ts @@ -0,0 +1,147 @@ +import { FocusKeyManager } from '@angular/cdk/a11y'; +import { LEFT_ARROW, RIGHT_ARROW, TAB } from '@angular/cdk/keycodes'; +import { + AfterContentInit, + ChangeDetectionStrategy, + Component, + ContentChildren, + ElementRef, + HostBinding, + Inject, + Input, + Optional, + QueryList, + ViewChild +} from '@angular/core'; +import { ANIMATION_MODULE_TYPE } from '@angular/platform-browser/animations'; + +import { CarouselItem } from './carousel-item.directive'; + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'gf-carousel', + styleUrls: ['./carousel.component.scss'], + templateUrl: './carousel.component.html' +}) +export class CarouselComponent implements AfterContentInit { + @ContentChildren(CarouselItem) public items!: QueryList; + + @HostBinding('class.animations-disabled') + public readonly animationsDisabled: boolean; + + @Input('aria-label') public ariaLabel: string | undefined; + + @ViewChild('list') public list!: ElementRef; + + public showPrevArrow = false; + public showNextArrow = true; + + private index = 0; + private keyManager!: FocusKeyManager; + private position = 0; + + public constructor( + @Optional() @Inject(ANIMATION_MODULE_TYPE) animationsModule?: string + ) { + this.animationsDisabled = animationsModule === 'NoopAnimations'; + } + + public ngAfterContentInit() { + this.keyManager = new FocusKeyManager(this.items); + } + + public next() { + for (let i = this.index; i < this.items.length; i++) { + if (this.isOutOfView(i)) { + this.index = i; + this.scrollToActiveItem(); + break; + } + } + } + + public onKeydown({ keyCode }: KeyboardEvent) { + const manager = this.keyManager; + const previousActiveIndex = manager.activeItemIndex; + + if (keyCode === LEFT_ARROW) { + manager.setPreviousItemActive(); + } else if (keyCode === RIGHT_ARROW) { + manager.setNextItemActive(); + } else if (keyCode === TAB && !manager.activeItem) { + manager.setFirstItemActive(); + } + + if ( + manager.activeItemIndex != null && + manager.activeItemIndex !== previousActiveIndex + ) { + this.index = manager.activeItemIndex; + this.updateItemTabIndices(); + + if (this.isOutOfView(this.index)) { + this.scrollToActiveItem(); + } + } + } + + public previous() { + for (let i = this.index; i > -1; i--) { + if (this.isOutOfView(i)) { + this.index = i; + this.scrollToActiveItem(); + break; + } + } + } + + private isOutOfView(index: number, side?: 'start' | 'end') { + const { offsetWidth, offsetLeft } = + this.items.toArray()[index].element.nativeElement; + + if ((!side || side === 'start') && offsetLeft - this.position < 0) { + return true; + } + + return ( + (!side || side === 'end') && + offsetWidth + offsetLeft - this.position > + this.list.nativeElement.clientWidth + ); + } + + private scrollToActiveItem() { + if (!this.isOutOfView(this.index)) { + return; + } + + const itemsArray = this.items.toArray(); + let targetItemIndex = this.index; + + if (this.index > 0 && !this.isOutOfView(this.index - 1)) { + targetItemIndex = + itemsArray.findIndex((_, i) => !this.isOutOfView(i)) + 1; + } + + this.position = + itemsArray[targetItemIndex].element.nativeElement.offsetLeft; + this.list.nativeElement.style.transform = `translateX(-${this.position}px)`; + this.showPrevArrow = this.index > 0; + this.showNextArrow = false; + + for (let i = itemsArray.length - 1; i > -1; i--) { + if (this.isOutOfView(i, 'end')) { + this.showNextArrow = true; + break; + } + } + } + + private updateItemTabIndices() { + this.items.forEach((item: CarouselItem) => { + if (this.keyManager != null) { + item.tabindex = item === this.keyManager.activeItem ? '0' : '-1'; + } + }); + } +} diff --git a/libs/ui/src/lib/carousel/carousel.html b/libs/ui/src/lib/carousel/carousel.html deleted file mode 100644 index efc91781c..000000000 --- a/libs/ui/src/lib/carousel/carousel.html +++ /dev/null @@ -1,16 +0,0 @@ - - - - - \ No newline at end of file diff --git a/libs/ui/src/lib/carousel/carousel.module.ts b/libs/ui/src/lib/carousel/carousel.module.ts new file mode 100644 index 000000000..4e43f23b0 --- /dev/null +++ b/libs/ui/src/lib/carousel/carousel.module.ts @@ -0,0 +1,14 @@ +import { CommonModule } from '@angular/common'; +import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; + +import { CarouselItem } from './carousel-item.directive'; +import { CarouselComponent } from './carousel.component'; + +@NgModule({ + declarations: [CarouselComponent, CarouselItem], + exports: [CarouselComponent, CarouselItem], + imports: [CommonModule, MatButtonModule], + schemas: [CUSTOM_ELEMENTS_SCHEMA] +}) +export class GfCarouselModule {} diff --git a/libs/ui/src/lib/carousel/carousel.scss b/libs/ui/src/lib/carousel/carousel.scss deleted file mode 100644 index 8993db95f..000000000 --- a/libs/ui/src/lib/carousel/carousel.scss +++ /dev/null @@ -1,37 +0,0 @@ -gf-carousel { - display: block; - position: relative; - } - - .docs-carousel-content { - display: flex; - flex-direction: row; - outline: none; - transition: transform 0.5s ease-in-out; - - .animations-disabled & { - transition: none; - } - } - - .docs-carousel-content-wrapper { - overflow: hidden; - } - - [carousel-item] { - flex-shrink: 0; - } - - button.docs-carousel-nav { - position: absolute; - top: 50%; - transform: translateY(-50%); - } - - .docs-carousel-nav-prev { - left: -50px; - } - - .docs-carousel-nav-next { - right: -50px; - } \ No newline at end of file diff --git a/libs/ui/src/lib/carousel/carousel.ts b/libs/ui/src/lib/carousel/carousel.ts deleted file mode 100644 index 400ed7a5a..000000000 --- a/libs/ui/src/lib/carousel/carousel.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { - AfterContentInit, - Component, - ContentChildren, - Directive, - ElementRef, - HostBinding, - Inject, - Input, - Optional, - QueryList, - ViewChild, - ViewEncapsulation, - } from '@angular/core'; - import {FocusableOption, FocusKeyManager} from '@angular/cdk/a11y'; - import {LEFT_ARROW, RIGHT_ARROW, TAB} from '@angular/cdk/keycodes'; - import {ANIMATION_MODULE_TYPE} from '@angular/platform-browser/animations'; - import {MatIconModule} from '@angular/material/icon'; - import {MatButtonModule} from '@angular/material/button'; - import {NgIf} from '@angular/common'; -import { CarouselItem } from './carousel-item.directive'; - - @Component({ - selector: 'gf-carousel', - imports: [ - NgIf, - MatButtonModule, - MatIconModule, - ], - }) - export class Carousel implements AfterContentInit { - @Input('aria-label') ariaLabel: string | undefined; - @ContentChildren(CarouselItem) items!: QueryList; - @ViewChild('list') list!: ElementRef; - @HostBinding('class.animations-disabled') readonly animationsDisabled: boolean; - position = 0; - showPrevArrow = false; - showNextArrow = true; - index = 0; - private _keyManager!: FocusKeyManager; - - onKeydown({keyCode}: KeyboardEvent) { - const manager = this._keyManager; - const previousActiveIndex = manager.activeItemIndex; - - if (keyCode === LEFT_ARROW) { - manager.setPreviousItemActive(); - } else if (keyCode === RIGHT_ARROW) { - manager.setNextItemActive(); - } else if (keyCode === TAB && !manager.activeItem) { - manager.setFirstItemActive(); - } - - if (manager.activeItemIndex != null && manager.activeItemIndex !== previousActiveIndex) { - this.index = manager.activeItemIndex; - this._updateItemTabIndices(); - - if (this._isOutOfView(this.index)) { - this._scrollToActiveItem(); - } - } - } - - constructor(@Optional() @Inject(ANIMATION_MODULE_TYPE) animationsModule?: string) { - this.animationsDisabled = animationsModule === 'NoopAnimations'; - } - - ngAfterContentInit(): void { - this._keyManager = new FocusKeyManager(this.items); - } - - /** Goes to the next set of items */ - next() { - for (let i = this.index; i < this.items.length; i++) { - if (this._isOutOfView(i)) { - this.index = i; - this._scrollToActiveItem(); - break; - } - } - } - - /** Goes to the previous set of items. */ - previous() { - for (let i = this.index; i > -1; i--) { - if (this._isOutOfView(i)) { - this.index = i; - this._scrollToActiveItem(); - break; - } - } - } - - /** Updates the `tabindex` of each of the items based on their active state. */ - private _updateItemTabIndices() { - this.items.forEach((item: CarouselItem) => { - if (this._keyManager != null) { - item.tabindex = item === this._keyManager.activeItem ? '0' : '-1'; - } - }); - } - - /** Scrolls an item into the viewport. */ - private _scrollToActiveItem() { - if (!this._isOutOfView(this.index)) { - return; - } - - const itemsArray = this.items.toArray(); - let targetItemIndex = this.index; - - // Only shift the carousel by one if we're going forwards. This - // looks better compared to moving the carousel by an entire page. - if (this.index > 0 && !this._isOutOfView(this.index - 1)) { - targetItemIndex = itemsArray.findIndex((_, i) => !this._isOutOfView(i)) + 1; - } - - this.position = itemsArray[targetItemIndex].element.nativeElement.offsetLeft; - this.list.nativeElement.style.transform = `translateX(-${this.position}px)`; - this.showPrevArrow = this.index > 0; - this.showNextArrow = false; - - for (let i = itemsArray.length - 1; i > -1; i--) { - if (this._isOutOfView(i, 'end')) { - this.showNextArrow = true; - break; - } - } - } - - /** Checks whether an item at a specific index is outside of the viewport. */ - private _isOutOfView(index: number, side?: 'start' | 'end') { - const {offsetWidth, offsetLeft} = this.items.toArray()[index].element.nativeElement; - - if ((!side || side === 'start') && offsetLeft - this.position < 0) { - return true; - } - - return ( - (!side || side === 'end') && - offsetWidth + offsetLeft - this.position > this.list.nativeElement.clientWidth - ); - } - } \ No newline at end of file diff --git a/libs/ui/src/lib/carousel/index.ts b/libs/ui/src/lib/carousel/index.ts new file mode 100644 index 000000000..2e039a80b --- /dev/null +++ b/libs/ui/src/lib/carousel/index.ts @@ -0,0 +1 @@ +export * from './carousel.module';