Initial commit
Dieser Commit ist enthalten in:
325
frontend/js/tour.js
Normale Datei
325
frontend/js/tour.js
Normale Datei
@ -0,0 +1,325 @@
|
||||
/**
|
||||
* 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;
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren