/** * TASKMATE - Tour/Onboarding Module * ================================== * First-time user onboarding tour */ import { $, createElement } from './utils.js'; class TourManager { constructor() { this.currentStep = 0; this.isActive = false; this.overlay = null; this.tooltip = null; this.steps = [ { target: '.view-tabs', title: 'Ansichten', content: 'Wechseln Sie zwischen Board-, Listen-, Kalender- und Dashboard-Ansicht.', position: 'bottom' }, { target: '.project-selector', title: 'Projekte', content: 'Wählen Sie ein Projekt aus oder erstellen Sie ein neues.', position: 'bottom' }, { target: '.column', title: 'Spalten', content: 'Spalten repräsentieren den Status Ihrer Aufgaben. Ziehen Sie Aufgaben zwischen Spalten, um den Status zu ändern.', position: 'right' }, { target: '.btn-add-task', title: 'Neue Aufgabe', content: 'Klicken Sie hier, um eine neue Aufgabe zu erstellen.', position: 'top' }, { target: '.filter-bar', title: 'Filter', content: 'Filtern Sie Aufgaben nach Priorität, Bearbeiter oder Fälligkeitsdatum.', position: 'bottom' }, { target: '#search-input', title: 'Suche', content: 'Durchsuchen Sie alle Aufgaben nach Titel oder Beschreibung. Tipp: Drücken Sie "/" für schnellen Zugriff.', position: 'bottom' }, { target: '.user-menu', title: 'Benutzermenu', content: 'Hier können Sie Ihr Passwort ändern oder sich abmelden.', position: 'bottom-left' }, { target: '#theme-toggle', title: 'Design', content: 'Wechseln Sie zwischen hellem und dunklem Design.', position: 'bottom-left' } ]; this.bindEvents(); } bindEvents() { window.addEventListener('tour:start', () => this.start()); window.addEventListener('tour:stop', () => this.stop()); // Keyboard navigation document.addEventListener('keydown', (e) => { if (!this.isActive) return; if (e.key === 'Escape') { this.stop(); } else if (e.key === 'ArrowRight' || e.key === 'Enter') { this.next(); } else if (e.key === 'ArrowLeft') { this.previous(); } }); } // ===================== // TOUR CONTROL // ===================== start() { // Check if tour was already completed if (localStorage.getItem('tour_completed') === 'true') { return; } this.isActive = true; this.currentStep = 0; this.createOverlay(); this.showStep(); } stop() { this.isActive = false; if (this.overlay) { this.overlay.remove(); this.overlay = null; } if (this.tooltip) { this.tooltip.remove(); this.tooltip = null; } // Remove highlight from any element $$('.tour-highlight')?.forEach(el => el.classList.remove('tour-highlight')); } complete() { localStorage.setItem('tour_completed', 'true'); this.stop(); window.dispatchEvent(new CustomEvent('toast:show', { detail: { message: 'Tour abgeschlossen! Viel Erfolg mit TaskMate.', type: 'success' } })); } next() { if (this.currentStep < this.steps.length - 1) { this.currentStep++; this.showStep(); } else { this.complete(); } } previous() { if (this.currentStep > 0) { this.currentStep--; this.showStep(); } } skip() { localStorage.setItem('tour_completed', 'true'); this.stop(); } // ===================== // UI CREATION // ===================== createOverlay() { this.overlay = createElement('div', { className: 'onboarding-overlay' }); document.body.appendChild(this.overlay); } showStep() { const step = this.steps[this.currentStep]; const targetElement = $(step.target); if (!targetElement) { // Skip to next step if target not found this.next(); return; } // Remove previous highlight $$('.tour-highlight')?.forEach(el => el.classList.remove('tour-highlight')); // Highlight current target targetElement.classList.add('tour-highlight'); // Position and show tooltip this.showTooltip(step, targetElement); } showTooltip(step, targetElement) { // Remove existing tooltip if (this.tooltip) { this.tooltip.remove(); } // Create tooltip this.tooltip = createElement('div', { className: 'onboarding-tooltip' }); // Content const content = createElement('div', { className: 'onboarding-content' }, [ createElement('h3', {}, [step.title]), createElement('p', {}, [step.content]) ]); this.tooltip.appendChild(content); // Footer const footer = createElement('div', { className: 'onboarding-footer' }); // Step indicator footer.appendChild(createElement('span', { id: 'onboarding-step' }, [`${this.currentStep + 1} / ${this.steps.length}`])); // Buttons const buttons = createElement('div', { className: 'onboarding-buttons' }); if (this.currentStep > 0) { buttons.appendChild(createElement('button', { className: 'btn btn-ghost', onclick: () => this.previous() }, ['Zurück'])); } buttons.appendChild(createElement('button', { className: 'btn btn-ghost', onclick: () => this.skip() }, ['Überspringen'])); const isLast = this.currentStep === this.steps.length - 1; buttons.appendChild(createElement('button', { className: 'btn btn-primary', onclick: () => this.next() }, [isLast ? 'Fertig' : 'Weiter'])); footer.appendChild(buttons); this.tooltip.appendChild(footer); document.body.appendChild(this.tooltip); // Position tooltip this.positionTooltip(targetElement, step.position); } positionTooltip(targetElement, position) { const targetRect = targetElement.getBoundingClientRect(); const tooltipRect = this.tooltip.getBoundingClientRect(); const padding = 16; let top, left; switch (position) { case 'top': top = targetRect.top - tooltipRect.height - padding; left = targetRect.left + (targetRect.width - tooltipRect.width) / 2; break; case 'bottom': top = targetRect.bottom + padding; left = targetRect.left + (targetRect.width - tooltipRect.width) / 2; break; case 'left': top = targetRect.top + (targetRect.height - tooltipRect.height) / 2; left = targetRect.left - tooltipRect.width - padding; break; case 'right': top = targetRect.top + (targetRect.height - tooltipRect.height) / 2; left = targetRect.right + padding; break; case 'bottom-left': top = targetRect.bottom + padding; left = targetRect.right - tooltipRect.width; break; case 'bottom-right': top = targetRect.bottom + padding; left = targetRect.left; break; default: top = targetRect.bottom + padding; left = targetRect.left; } // Keep within viewport const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; if (left < padding) left = padding; if (left + tooltipRect.width > viewportWidth - padding) { left = viewportWidth - tooltipRect.width - padding; } if (top < padding) top = padding; if (top + tooltipRect.height > viewportHeight - padding) { top = viewportHeight - tooltipRect.height - padding; } this.tooltip.style.top = `${top}px`; this.tooltip.style.left = `${left}px`; } // ===================== // HELPERS // ===================== shouldShowTour() { return localStorage.getItem('tour_completed') !== 'true'; } resetTour() { localStorage.removeItem('tour_completed'); } } // Helper function for querying multiple elements function $$(selector) { return Array.from(document.querySelectorAll(selector)); } // Create and export singleton const tourManager = new TourManager(); export default tourManager;