diff --git a/.github/workflows/deploy-gh-pages.yml b/.github/workflows/deploy-gh-pages.yml new file mode 100644 index 0000000000..ba0f2e3f9e --- /dev/null +++ b/.github/workflows/deploy-gh-pages.yml @@ -0,0 +1,36 @@ +name: Deploy Demo to GitHub Pages + +on: + push: + branches: + - demo/guided-tour-gh-pages + +permissions: + contents: write + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout 🛎️ + uses: actions/checkout@v4 + + - name: Setup Node.js ⚙️ + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Install Dependencies 📦 + run: npm install --legacy-peer-deps + + - name: Build Library 🏗️ + run: npm run build:ui + + - name: Build Demo App 🚀 + run: npx ng build app --base-href /po-angular/ + + - name: Deploy to GitHub Pages 🚀 + uses: JamesIves/github-pages-deploy-action@v4 + with: + folder: dist/app + branch: gh-pages diff --git a/projects/app/src/app/app.component.html b/projects/app/src/app/app.component.html index e69de29bb2..aa52ab50ed 100644 --- a/projects/app/src/app/app.component.html +++ b/projects/app/src/app/app.component.html @@ -0,0 +1,34 @@ +
+ + + +
+ +
+ +
+ +

Este card é o primeiro passo do nosso tour. Ele serve para dar as boas-vindas aos usuários.

+
+ + +

Clique no botão abaixo para iniciar a experiência guiada.

+ +
+
+ +
+ +

O tour guiado ajuda a aumentar o engajamento e diminuir a curva de aprendizado da sua aplicação.

+
+
+
+ + + +
diff --git a/projects/app/src/app/app.component.ts b/projects/app/src/app/app.component.ts index 7bf08e2511..d969f989b4 100644 --- a/projects/app/src/app/app.component.ts +++ b/projects/app/src/app/app.component.ts @@ -1,8 +1,52 @@ -import { Component } from '@angular/core'; +import { Component, ViewChild } from '@angular/core'; +import { PoGuidedTourStep } from './../../ui/src/lib/components/po-guided-tour/po-guided-tour.component'; @Component({ selector: 'app-root', templateUrl: './app.component.html', standalone: false }) -export class AppComponent {} +export class AppComponent { + + @ViewChild(PoGuidedTourComponent) guidedTour: PoGuidedTourComponent; + + readonly tourSteps: Array = [ + { + id: 'step-welcome', + element: '#welcome-card', + title: 'Boas-vindas!', + description: 'Olá! Este é o novo componente de tour guiado do PO UI. Vamos conhecer algumas funcionalidades?', + position: 'bottom' + }, + { + id: 'step-menu', + element: '.po-menu-item-link', + title: 'Navegação Fácil', + description: 'Aqui você pode navegar por todas as áreas da nossa aplicação de demonstração.', + position: 'right' + }, + { + id: 'step-action', + element: '#action-btn', + title: 'Ações Contextuais', + description: 'Você pode disparar ações importantes diretamente daqui.', + position: 'left' + }, + { + id: 'step-final', + element: '#final-card', + title: 'Pronto para começar?', + description: 'Agora que você conhece o básico, explore à vontade o nosso Design System!', + position: 'top' + } + ]; + + startTour() { + this.guidedTour.start(); + } + + onTourComplete() { + console.log('Tour finalizado com sucesso!'); + } + +} diff --git a/projects/app/src/app/app.module.ts b/projects/app/src/app/app.module.ts index c4d03b9a2c..e54bd723d5 100644 --- a/projects/app/src/app/app.module.ts +++ b/projects/app/src/app/app.module.ts @@ -6,11 +6,19 @@ import { RouterModule } from '@angular/router'; import { PoModule } from '@po-ui/ng-components'; import { AppComponent } from './app.component'; - +import { PoGuidedTourModule } from '@po-ui/ng-components'; @NgModule({ - declarations: [AppComponent], + declarations: [ + AppComponent, + ], bootstrap: [AppComponent], - imports: [BrowserModule, FormsModule, RouterModule.forRoot([], {}), PoModule], + imports: [ + BrowserModule, + FormsModule, + RouterModule.forRoot([], {}), + PoModule, + PoGuidedTourModule + ], providers: [provideHttpClient(withInterceptorsFromDi())] }) export class AppModule {} diff --git a/projects/ui/src/lib/components/components.module.ts b/projects/ui/src/lib/components/components.module.ts index c79c642c4d..c5009dff11 100644 --- a/projects/ui/src/lib/components/components.module.ts +++ b/projects/ui/src/lib/components/components.module.ts @@ -18,6 +18,7 @@ import { PoFieldModule } from './po-field/po-field.module'; import { PoSwitchModule } from './po-field/po-switch/po-switch.module'; import { PoGaugeModule } from './po-gauge/po-gauge.module'; import { PoGridModule } from './po-grid/po-grid.module'; +import { PoGuidedTourModule } from './po-guided-tour/po-guided-tour.module'; import { PoIconModule } from './po-icon/po-icon.module'; import { PoImageModule } from './po-image/po-image.module'; import { PoInfoModule } from './po-info/po-info.module'; @@ -68,6 +69,7 @@ const PO_MODULES = [ PoFieldModule, PoGaugeModule, PoGridModule, + PoGuidedTourModule, PoIconModule, PoInfoModule, PoListViewModule, diff --git a/projects/ui/src/lib/components/index.ts b/projects/ui/src/lib/components/index.ts index edb080fd33..65eeb8abb6 100644 --- a/projects/ui/src/lib/components/index.ts +++ b/projects/ui/src/lib/components/index.ts @@ -17,6 +17,7 @@ export * from './po-dynamic/index'; export * from './po-field/index'; export * from './po-gauge/index'; export * from './po-grid/index'; +export * from './po-guided-tour/index'; export * from './po-helper/index'; export * from './po-icon/index'; export * from './po-image/index'; diff --git a/projects/ui/src/lib/components/po-guided-tour/index.ts b/projects/ui/src/lib/components/po-guided-tour/index.ts new file mode 100644 index 0000000000..c78dab971a --- /dev/null +++ b/projects/ui/src/lib/components/po-guided-tour/index.ts @@ -0,0 +1,2 @@ +export * from './po-guided-tour.module'; +export * from './po-guided-tour.component'; diff --git a/projects/ui/src/lib/components/po-guided-tour/po-guided-tour.component.html b/projects/ui/src/lib/components/po-guided-tour/po-guided-tour.component.html new file mode 100644 index 0000000000..fd4ac52f6b --- /dev/null +++ b/projects/ui/src/lib/components/po-guided-tour/po-guided-tour.component.html @@ -0,0 +1,54 @@ +
+ +
+
+

{{ getCurrentStep()?.title }}

+ +
+ +
+

{{ getCurrentStep()?.description }}

+
+ +
+
+
+ + +
+
diff --git a/projects/ui/src/lib/components/po-guided-tour/po-guided-tour.component.scss b/projects/ui/src/lib/components/po-guided-tour/po-guided-tour.component.scss new file mode 100644 index 0000000000..0296d9389d --- /dev/null +++ b/projects/ui/src/lib/components/po-guided-tour/po-guided-tour.component.scss @@ -0,0 +1,19 @@ +.po-guided-tour-overlay { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background-color: rgba(0, 0, 0, 0.7); + pointer-events: none; + overflow: hidden; + z-index: 9999; +} + +.po-guided-tour-spotlight { + position: absolute; + box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.7); + border-radius: 4px; + transition: all 0.3s ease-in-out; + pointer-events: auto; +} diff --git a/projects/ui/src/lib/components/po-guided-tour/po-guided-tour.component.ts b/projects/ui/src/lib/components/po-guided-tour/po-guided-tour.component.ts new file mode 100644 index 0000000000..e04b6544a2 --- /dev/null +++ b/projects/ui/src/lib/components/po-guided-tour/po-guided-tour.component.ts @@ -0,0 +1,241 @@ +import { Component, ViewChild, ElementRef, OnInit, OnDestroy, Input, Output, EventEmitter, ChangeDetectionStrategy, ChangeDetectorRef, Renderer2 } from '@angular/core'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; +export interface PoGuidedTourStep { + id: string; + element: string; + title: string; + description: string; + position?: 'top' | 'bottom' | 'left' | 'right' | 'center'; + blockInteraction?: boolean; + onEnter?: () => void; + onLeave?: () => void; +} + +export interface PoGuidedTourConfig { + steps: PoGuidedTourStep[]; + title?: string; + allowClose?: boolean; + allowSkip?: boolean; + storageKey?: string; + zIndex?: number; + onComplete?: () => void; + onCancel?: () => void; +} + +@Component({ + selector: 'po-guided-tour', + templateUrl: './po-guided-tour.component.html', + styleUrls: ['./po-guided-tour.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class PoGuidedTourComponent implements OnInit, OnDestroy { + @ViewChild('popover') popover: any; + + private _steps: PoGuidedTourStep[] = []; + private _allowClose = true; + private _allowSkip = true; + private _storageKey = 'po-guided-tour-completed'; + private _zIndex = 9999; + + currentStep = 0; + isActive = false; + highlightedElement: HTMLElement | null = null; + overlay: HTMLElement | null = null; + + private destroy$ = new Subject(); + + @Input('p-steps') set steps(value: PoGuidedTourStep[]) { + this._steps = value || []; + } + get steps(): PoGuidedTourStep[] { + return this._steps; + } + + @Input('p-allow-close') set allowClose(value: boolean) { + this._allowClose = value !== false; + } + get allowClose(): boolean { + return this._allowClose; + } + + @Input('p-allow-skip') set allowSkip(value: boolean) { + this._allowSkip = value !== false; + } + + get allowSkip(): boolean { + return this._allowSkip; + } + + @Input('p-storage-key') set storageKey(value: string) { + this._storageKey = value || 'po-guided-tour-completed'; + } + get storageKey(): string { + return this._storageKey; + } + + @Input('p-z-index') set zIndex(value: number) { + this._zIndex = value || 9999; + } + get zIndex(): number { + return this._zIndex; + } + + @Output('p-on-complete') onComplete = new EventEmitter(); + @Output('p-on-cancel') onCancel = new EventEmitter(); + @Output('p-on-step-change') onStepChange = new EventEmitter(); + + constructor( + private changeDetectorRef: ChangeDetectorRef, + private renderer: Renderer2, + private elementRef: ElementRef + ) {} + + ngOnInit(): void { + this.loadStorageState(); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + this.cleanup(); + } + + start(): void { + if (this.isAlreadyCompleted()) { + return; + } + this.isActive = true; + this.currentStep = 0; + this.changeDetectorRef.markForCheck(); + this.goToStep(0); + } + + next(): void { + if (this.currentStep < this.steps.length - 1) { + this.goToStep(this.currentStep + 1); + } else { + this.complete(); + } + } + + previous(): void { + if (this.currentStep > 0) { + this.goToStep(this.currentStep - 1); + } + } + + goToStep(stepIndex: number): void { + if (stepIndex < 0 || stepIndex >= this.steps.length) { + return; + } + const previousStep = this.steps[this.currentStep]; + if (previousStep && previousStep.onLeave) { + previousStep.onLeave(); + } + + this.currentStep = stepIndex; + const step = this.steps[stepIndex]; + if (step.onEnter) { + step.onEnter(); + } + + this.highlightElement(step.element); + this.onStepChange.emit(stepIndex); + this.changeDetectorRef.markForCheck(); + } + + complete(): void { + this.saveToStorage(); + this.isActive = false; + this.cleanup(); + this.onComplete.emit(); + this.changeDetectorRef.markForCheck(); + } + + cancel(): void { + this.isActive = false; + this.cleanup(); + this.onCancel.emit(); + this.changeDetectorRef.markForCheck(); + } + + skipTour(): void { + this.complete(); + } + + getCurrentStep(): PoGuidedTourStep { + return this.steps[this.currentStep]; + } + + isFirstStep(): boolean { + return this.currentStep === 0; + } + + isLastStep(): boolean { + return this.currentStep === this.steps.length - 1; + } + + getProgress(): number { + return Math.round(((this.currentStep + 1) / this.steps.length) * 100); + } + + private highlightElement(selector: string): void { + const element = document.querySelector(selector); + if (!element) { + console.warn(`[PoGuidedTour] Elemento não encontrado: ${selector}`); + return; + } + + this.highlightedElement = element as HTMLElement; + this.createOverlay(); + this.scrollToElement(this.highlightedElement); + } + + private createOverlay(): void { + if (this.overlay) { + this.overlay.remove(); + } + + this.overlay = this.renderer.createElement('div'); + this.renderer.addClass(this.overlay, 'po-guided-tour-overlay'); + this.renderer.setStyle(this.overlay, 'z-index', this._zIndex.toString()); + this.renderer.appendChild(document.body, this.overlay); + + if (this.highlightedElement && this.overlay) { + const rect = this.highlightedElement.getBoundingClientRect(); + const spotlightCircle = this.renderer.createElement('div'); + this.renderer.addClass(spotlightCircle, 'po-guided-tour-spotlight'); + this.renderer.setStyle(spotlightCircle, 'left', `${window.scrollX + rect.left}px`); + this.renderer.setStyle(spotlightCircle, 'top', `${window.scrollY + rect.top}px`); + this.renderer.setStyle(spotlightCircle, 'width', `${rect.width}px`); + this.renderer.setStyle(spotlightCircle, 'height', `${rect.height}px`); + this.renderer.appendChild(this.overlay, spotlightCircle); + } + } + + private scrollToElement(element: HTMLElement): void { + if (element) { + element.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'center' }); + } + } + + private isAlreadyCompleted(): boolean { + return localStorage.getItem(this.storageKey) === 'true'; + } + + private saveToStorage(): void { + localStorage.setItem(this.storageKey, 'true'); + } + + private loadStorageState(): void { + } + + private cleanup(): void { + if (this.overlay) { + this.overlay.remove(); + this.overlay = null; + } + this.highlightedElement = null; + } +} diff --git a/projects/ui/src/lib/components/po-guided-tour/po-guided-tour.module.ts b/projects/ui/src/lib/components/po-guided-tour/po-guided-tour.module.ts new file mode 100644 index 0000000000..68610777c0 --- /dev/null +++ b/projects/ui/src/lib/components/po-guided-tour/po-guided-tour.module.ts @@ -0,0 +1,16 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { PoGuidedTourComponent } from './po-guided-tour.component'; + +@NgModule({ + imports: [ + CommonModule + ], + declarations: [ + PoGuidedTourComponent + ], + exports: [ + PoGuidedTourComponent + ] +}) +export class PoGuidedTourModule { }