Files
TaskMate/frontend/js/tour.js
Claude Project Manager ab1e5be9a9 Initial commit
2025-12-28 21:36:45 +00:00

326 Zeilen
8.1 KiB
JavaScript

/**
* 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;