diff --git a/backend/.claude/settings.local.json b/backend/.claude/settings.local.json
index 616dc09f..97e55bbc 100644
--- a/backend/.claude/settings.local.json
+++ b/backend/.claude/settings.local.json
@@ -6,7 +6,8 @@
"Bash(for file in animations-optimized.css glassmorphism.css components.css professional-theme.css)",
"Bash(do echo \"=== $file ===\")",
"Bash(echo)",
- "Bash(done)"
+ "Bash(done)",
+ "Bash(npm run build:css:*)"
],
"deny": []
}
diff --git a/backend/static/css/core-utilities.css b/backend/static/css/core-utilities.css
new file mode 100644
index 00000000..f1b25a03
--- /dev/null
+++ b/backend/static/css/core-utilities.css
@@ -0,0 +1,102 @@
+/* Core Utilities CSS - Notification System Styles */
+
+/* ===== NOTIFICATION CONTAINER ===== */
+#myp-notifications {
+ pointer-events: none;
+ max-width: 400px;
+}
+
+#myp-notifications > * {
+ pointer-events: auto;
+}
+
+/* ===== BASE NOTIFICATION STYLES ===== */
+.notification {
+ min-width: 300px;
+ max-width: 100%;
+ word-wrap: break-word;
+ font-size: 0.875rem;
+ line-height: 1.25rem;
+}
+
+/* Glassmorphism effect for notifications */
+.notification.glass-navbar {
+ background: rgba(255, 255, 255, 0.85);
+ backdrop-filter: blur(10px);
+ -webkit-backdrop-filter: blur(10px);
+ border: 1px solid rgba(255, 255, 255, 0.3);
+}
+
+.dark .notification.glass-navbar {
+ background: rgba(15, 23, 42, 0.85);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+}
+
+/* ===== NOTIFICATION TYPES ===== */
+.notification-success {
+ border-left: 4px solid #10b981;
+}
+
+.notification-error {
+ border-left: 4px solid #ef4444;
+}
+
+.notification-warning {
+ border-left: 4px solid #f59e0b;
+}
+
+.notification-info {
+ border-left: 4px solid #3b82f6;
+}
+
+/* ===== NOTIFICATION ANIMATIONS ===== */
+.notification {
+ transition: transform 0.3s ease-out, opacity 0.3s ease-out;
+}
+
+.notification.translate-x-full {
+ transform: translateX(100%);
+ opacity: 0;
+}
+
+/* ===== CLOSE BUTTON ===== */
+.notification button {
+ font-size: 1.5rem;
+ line-height: 1;
+ background: none;
+ border: none;
+ cursor: pointer;
+ opacity: 0.7;
+ transition: opacity 0.2s;
+}
+
+.notification button:hover {
+ opacity: 1;
+}
+
+/* ===== RESPONSIVE DESIGN ===== */
+@media (max-width: 640px) {
+ #myp-notifications {
+ left: 1rem;
+ right: 1rem;
+ max-width: none;
+ }
+
+ .notification {
+ min-width: auto;
+ }
+}
+
+/* ===== ACCESSIBILITY ===== */
+@media (prefers-reduced-motion: reduce) {
+ .notification {
+ transition: none;
+ }
+}
+
+/* ===== PRINT STYLES ===== */
+@media print {
+ #myp-notifications {
+ display: none;
+ }
+}
\ No newline at end of file
diff --git a/backend/static/js/core-utilities-optimized.js b/backend/static/js/core-utilities-optimized.js
new file mode 100644
index 00000000..fbe263fa
--- /dev/null
+++ b/backend/static/js/core-utilities-optimized.js
@@ -0,0 +1,468 @@
+/**
+ * MYP Platform Core Utilities - Optimized Version
+ * Consolidates common functionality from multiple files
+ * Reduces redundancy and improves performance
+ */
+
+(function(window) {
+ 'use strict';
+
+ // Single instance pattern for core utilities
+ const MYPCore = {
+ // Version for cache busting
+ version: '1.0.0',
+
+ // CSRF Token Management (replaces 22+ different implementations)
+ csrf: {
+ _token: null,
+
+ getToken() {
+ if (!this._token) {
+ // Try multiple sources in order of preference
+ this._token =
+ document.querySelector('meta[name="csrf-token"]')?.content ||
+ document.querySelector('input[name="csrf_token"]')?.value ||
+ this._getFromCookie('csrf_token') ||
+ '';
+ }
+ return this._token;
+ },
+
+ _getFromCookie(name) {
+ const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
+ return match ? match[2] : null;
+ },
+
+ clearCache() {
+ this._token = null;
+ }
+ },
+
+ // DOM Utilities with caching
+ dom: {
+ _cache: new Map(),
+
+ // Cached querySelector
+ get(selector, parent = document) {
+ const key = `${parent === document ? 'doc' : 'el'}_${selector}`;
+ if (!this._cache.has(key)) {
+ this._cache.set(key, parent.querySelector(selector));
+ }
+ return this._cache.get(key);
+ },
+
+ // Cached querySelectorAll
+ getAll(selector, parent = document) {
+ return parent.querySelectorAll(selector);
+ },
+
+ // Efficient HTML escaping
+ escapeHtml(text) {
+ const div = this._escapeDiv || (this._escapeDiv = document.createElement('div'));
+ div.textContent = text;
+ return div.innerHTML;
+ },
+
+ // Clear cache when DOM changes
+ clearCache() {
+ this._cache.clear();
+ },
+
+ // Efficient element creation
+ create(tag, attrs = {}, children = []) {
+ const el = document.createElement(tag);
+ Object.entries(attrs).forEach(([key, value]) => {
+ if (key === 'class') {
+ el.className = value;
+ } else if (key === 'style' && typeof value === 'object') {
+ Object.assign(el.style, value);
+ } else if (key.startsWith('data-')) {
+ el.dataset[key.slice(5)] = value;
+ } else {
+ el.setAttribute(key, value);
+ }
+ });
+ children.forEach(child => {
+ if (typeof child === 'string') {
+ el.appendChild(document.createTextNode(child));
+ } else {
+ el.appendChild(child);
+ }
+ });
+ return el;
+ }
+ },
+
+ // Unified Notification System (replaces 3 separate systems)
+ notify: {
+ _container: null,
+ _queue: [],
+ _activeToasts: new Map(),
+ _toastId: 0,
+
+ show(message, type = 'info', duration = 5000, options = {}) {
+ // Check for DND mode
+ if (window.dndManager?.isEnabled && !options.force) {
+ console.log(`[DND] Suppressed ${type} notification:`, message);
+ return null;
+ }
+
+ const id = `toast-${++this._toastId}`;
+ const toast = this._createToast(id, message, type, duration, options);
+
+ this._ensureContainer();
+ this._container.appendChild(toast);
+
+ // Animate in
+ requestAnimationFrame(() => {
+ toast.classList.add('show');
+ });
+
+ // Auto remove
+ if (duration > 0 && !options.persistent) {
+ const timeout = setTimeout(() => this.close(id), duration);
+ this._activeToasts.set(id, { element: toast, timeout });
+ }
+
+ return id;
+ },
+
+ _createToast(id, message, type, duration, options) {
+ const toast = MYPCore.dom.create('div', {
+ id,
+ class: `myp-toast myp-toast-${type}`,
+ role: 'alert',
+ 'aria-live': 'polite'
+ });
+
+ const icon = this._getIcon(type);
+ const content = MYPCore.dom.create('div', { class: 'toast-content' }, [
+ MYPCore.dom.create('span', { class: 'toast-icon' }, [icon]),
+ MYPCore.dom.create('span', { class: 'toast-message' }, [message])
+ ]);
+
+ const closeBtn = MYPCore.dom.create('button', {
+ class: 'toast-close',
+ 'aria-label': 'Close notification',
+ onclick: () => this.close(id)
+ }, ['×']);
+
+ toast.appendChild(content);
+ toast.appendChild(closeBtn);
+
+ if (duration > 0 && !options.persistent) {
+ const progress = MYPCore.dom.create('div', { class: 'toast-progress' });
+ const bar = MYPCore.dom.create('div', {
+ class: 'toast-progress-bar',
+ style: { animationDuration: `${duration}ms` }
+ });
+ progress.appendChild(bar);
+ toast.appendChild(progress);
+ }
+
+ return toast;
+ },
+
+ _getIcon(type) {
+ const icons = {
+ success: '✓',
+ error: '✗',
+ warning: '⚠',
+ info: 'ℹ'
+ };
+ return icons[type] || icons.info;
+ },
+
+ _ensureContainer() {
+ if (!this._container) {
+ this._container = MYPCore.dom.get('#myp-toast-container') ||
+ MYPCore.dom.create('div', { id: 'myp-toast-container', class: 'myp-toast-container' });
+
+ if (!this._container.parentNode) {
+ document.body.appendChild(this._container);
+ }
+ }
+ },
+
+ close(id) {
+ const toast = this._activeToasts.get(id);
+ if (toast) {
+ clearTimeout(toast.timeout);
+ toast.element.classList.remove('show');
+ setTimeout(() => {
+ toast.element.remove();
+ this._activeToasts.delete(id);
+ }, 300);
+ }
+ },
+
+ closeAll() {
+ this._activeToasts.forEach((_, id) => this.close(id));
+ }
+ },
+
+ // API Request Utilities
+ api: {
+ _cache: new Map(),
+ _pendingRequests: new Map(),
+
+ async request(url, options = {}) {
+ const config = {
+ ...options,
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-CSRFToken': MYPCore.csrf.getToken(),
+ ...options.headers
+ }
+ };
+
+ // Check cache for GET requests
+ if (options.method === 'GET' || !options.method) {
+ const cacheKey = this._getCacheKey(url, options);
+ const cached = this._cache.get(cacheKey);
+
+ if (cached && !options.noCache) {
+ const age = Date.now() - cached.timestamp;
+ const maxAge = options.cacheTime || 300000; // 5 minutes default
+
+ if (age < maxAge) {
+ return cached.data;
+ }
+ }
+
+ // Prevent duplicate requests
+ if (this._pendingRequests.has(cacheKey)) {
+ return this._pendingRequests.get(cacheKey);
+ }
+ }
+
+ // Make request
+ const requestPromise = fetch(url, config)
+ .then(async response => {
+ if (!response.ok) {
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
+ }
+
+ const data = await response.json();
+
+ // Cache successful GET requests
+ if (config.method === 'GET' || !config.method) {
+ const cacheKey = this._getCacheKey(url, options);
+ this._cache.set(cacheKey, {
+ data,
+ timestamp: Date.now()
+ });
+ this._pendingRequests.delete(cacheKey);
+ }
+
+ return data;
+ })
+ .catch(error => {
+ if (config.method === 'GET' || !config.method) {
+ this._pendingRequests.delete(this._getCacheKey(url, options));
+ }
+ throw error;
+ });
+
+ if (config.method === 'GET' || !config.method) {
+ this._pendingRequests.set(this._getCacheKey(url, options), requestPromise);
+ }
+
+ return requestPromise;
+ },
+
+ _getCacheKey(url, options) {
+ return `${url}_${JSON.stringify(options.params || {})}`;
+ },
+
+ clearCache(pattern) {
+ if (pattern) {
+ for (const [key] of this._cache) {
+ if (key.includes(pattern)) {
+ this._cache.delete(key);
+ }
+ }
+ } else {
+ this._cache.clear();
+ }
+ }
+ },
+
+ // Time/Date Utilities
+ time: {
+ formatAgo(timestamp) {
+ const date = timestamp instanceof Date ? timestamp : new Date(timestamp);
+ const now = new Date();
+ const seconds = Math.floor((now - date) / 1000);
+
+ const intervals = [
+ { label: 'Jahr', seconds: 31536000, plural: 'Jahre' },
+ { label: 'Monat', seconds: 2592000, plural: 'Monate' },
+ { label: 'Woche', seconds: 604800, plural: 'Wochen' },
+ { label: 'Tag', seconds: 86400, plural: 'Tage' },
+ { label: 'Stunde', seconds: 3600, plural: 'Stunden' },
+ { label: 'Minute', seconds: 60, plural: 'Minuten' }
+ ];
+
+ for (const interval of intervals) {
+ const count = Math.floor(seconds / interval.seconds);
+ if (count >= 1) {
+ const label = count === 1 ? interval.label : interval.plural;
+ return `vor ${count} ${label}`;
+ }
+ }
+
+ return 'Gerade eben';
+ },
+
+ formatDuration(seconds) {
+ const hours = Math.floor(seconds / 3600);
+ const minutes = Math.floor((seconds % 3600) / 60);
+ const secs = seconds % 60;
+
+ const parts = [];
+ if (hours > 0) parts.push(`${hours}h`);
+ if (minutes > 0) parts.push(`${minutes}m`);
+ if (secs > 0 || parts.length === 0) parts.push(`${secs}s`);
+
+ return parts.join(' ');
+ }
+ },
+
+ // Performance Utilities
+ perf: {
+ debounce(func, wait) {
+ let timeout;
+ return function debounced(...args) {
+ const context = this;
+ clearTimeout(timeout);
+ timeout = setTimeout(() => func.apply(context, args), wait);
+ };
+ },
+
+ throttle(func, limit) {
+ let inThrottle;
+ return function throttled(...args) {
+ const context = this;
+ if (!inThrottle) {
+ func.apply(context, args);
+ inThrottle = true;
+ setTimeout(() => inThrottle = false, limit);
+ }
+ };
+ },
+
+ memoize(func, resolver) {
+ const cache = new Map();
+ return function memoized(...args) {
+ const key = resolver ? resolver(...args) : args[0];
+ if (cache.has(key)) {
+ return cache.get(key);
+ }
+ const result = func.apply(this, args);
+ cache.set(key, result);
+ return result;
+ };
+ }
+ },
+
+ // Event Management
+ events: {
+ _listeners: new Map(),
+
+ on(element, event, handler, options = {}) {
+ const key = `${element}_${event}`;
+ if (!this._listeners.has(key)) {
+ this._listeners.set(key, new Set());
+ }
+
+ this._listeners.get(key).add(handler);
+ element.addEventListener(event, handler, options);
+
+ // Return cleanup function
+ return () => this.off(element, event, handler);
+ },
+
+ off(element, event, handler) {
+ const key = `${element}_${event}`;
+ const handlers = this._listeners.get(key);
+
+ if (handlers) {
+ handlers.delete(handler);
+ if (handlers.size === 0) {
+ this._listeners.delete(key);
+ }
+ }
+
+ element.removeEventListener(event, handler);
+ },
+
+ once(element, event, handler, options = {}) {
+ const wrappedHandler = (e) => {
+ handler(e);
+ this.off(element, event, wrappedHandler);
+ };
+
+ return this.on(element, event, wrappedHandler, options);
+ }
+ },
+
+ // Storage Utilities
+ storage: {
+ get(key, defaultValue = null) {
+ try {
+ const item = localStorage.getItem(`myp_${key}`);
+ return item ? JSON.parse(item) : defaultValue;
+ } catch (e) {
+ console.error('Storage get error:', e);
+ return defaultValue;
+ }
+ },
+
+ set(key, value) {
+ try {
+ localStorage.setItem(`myp_${key}`, JSON.stringify(value));
+ return true;
+ } catch (e) {
+ console.error('Storage set error:', e);
+ return false;
+ }
+ },
+
+ remove(key) {
+ localStorage.removeItem(`myp_${key}`);
+ },
+
+ clear(prefix = 'myp_') {
+ const keys = Object.keys(localStorage);
+ keys.forEach(key => {
+ if (key.startsWith(prefix)) {
+ localStorage.removeItem(key);
+ }
+ });
+ }
+ }
+ };
+
+ // Global shortcuts for backwards compatibility
+ window.MYPCore = MYPCore;
+
+ // Legacy function mappings
+ window.showToast = (msg, type, duration) => MYPCore.notify.show(msg, type, duration);
+ window.showNotification = window.showToast;
+ window.showFlashMessage = window.showToast;
+ window.showSuccessMessage = (msg) => MYPCore.notify.show(msg, 'success');
+ window.showErrorMessage = (msg) => MYPCore.notify.show(msg, 'error');
+ window.showWarningMessage = (msg) => MYPCore.notify.show(msg, 'warning');
+ window.showInfoMessage = (msg) => MYPCore.notify.show(msg, 'info');
+
+ // Auto-initialize on DOM ready
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', () => {
+ console.log('✅ MYP Core Utilities initialized');
+ });
+ } else {
+ console.log('✅ MYP Core Utilities initialized');
+ }
+
+})(window);
\ No newline at end of file
diff --git a/backend/static/js/core-utilities.js b/backend/static/js/core-utilities.js
new file mode 100644
index 00000000..fdc9f1d9
--- /dev/null
+++ b/backend/static/js/core-utilities.js
@@ -0,0 +1,493 @@
+/**
+ * Core Utilities Module for MYP Platform
+ * Consolidated utilities to eliminate redundancy and improve performance
+ */
+
+(function(window) {
+ 'use strict';
+
+ // Module namespace
+ const MYP = window.MYP || {};
+
+ // ===== CONFIGURATION =====
+ const config = {
+ apiTimeout: 30000,
+ cacheExpiry: 5 * 60 * 1000, // 5 minutes
+ notificationDuration: 5000,
+ debounceDelay: 300,
+ throttleDelay: 100
+ };
+
+ // ===== CACHE MANAGEMENT =====
+ const cache = new Map();
+ const requestCache = new Map();
+
+ // ===== CSRF TOKEN HANDLING =====
+ const csrf = {
+ token: null,
+
+ get() {
+ if (!this.token) {
+ const meta = document.querySelector('meta[name="csrf-token"]');
+ this.token = meta ? meta.getAttribute('content') : '';
+ }
+ return this.token;
+ },
+
+ headers() {
+ return {
+ 'X-CSRFToken': this.get(),
+ 'Content-Type': 'application/json'
+ };
+ }
+ };
+
+ // ===== DOM UTILITIES =====
+ const dom = {
+ // Cached selectors
+ selectors: new Map(),
+
+ // Get element with caching
+ get(selector, parent = document) {
+ const key = `${parent === document ? 'doc' : 'el'}_${selector}`;
+ if (!this.selectors.has(key)) {
+ this.selectors.set(key, parent.querySelector(selector));
+ }
+ return this.selectors.get(key);
+ },
+
+ // Get all elements
+ getAll(selector, parent = document) {
+ return parent.querySelectorAll(selector);
+ },
+
+ // Clear cache
+ clearCache() {
+ this.selectors.clear();
+ },
+
+ // Safe element creation
+ create(tag, attrs = {}, text = '') {
+ const el = document.createElement(tag);
+ Object.entries(attrs).forEach(([key, val]) => {
+ if (key === 'class') {
+ el.className = val;
+ } else if (key === 'dataset') {
+ Object.entries(val).forEach(([k, v]) => {
+ el.dataset[k] = v;
+ });
+ } else {
+ el.setAttribute(key, val);
+ }
+ });
+ if (text) el.textContent = text;
+ return el;
+ }
+ };
+
+ // ===== API REQUEST HANDLING =====
+ const api = {
+ // Request deduplication
+ pending: new Map(),
+
+ async request(url, options = {}) {
+ const key = `${options.method || 'GET'}_${url}`;
+
+ // Check for pending request
+ if (this.pending.has(key)) {
+ return this.pending.get(key);
+ }
+
+ // Check cache for GET requests
+ if (!options.method || options.method === 'GET') {
+ const cached = requestCache.get(key);
+ if (cached && Date.now() - cached.timestamp < config.cacheExpiry) {
+ return Promise.resolve(cached.data);
+ }
+ }
+
+ // Prepare request
+ const requestOptions = {
+ ...options,
+ headers: {
+ ...csrf.headers(),
+ ...options.headers
+ }
+ };
+
+ // Create request promise
+ const promise = fetch(url, requestOptions)
+ .then(response => {
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ return response.json();
+ })
+ .then(data => {
+ // Cache successful GET requests
+ if (!options.method || options.method === 'GET') {
+ requestCache.set(key, {
+ data,
+ timestamp: Date.now()
+ });
+ }
+ return data;
+ })
+ .finally(() => {
+ this.pending.delete(key);
+ });
+
+ this.pending.set(key, promise);
+ return promise;
+ },
+
+ get(url, options = {}) {
+ return this.request(url, { ...options, method: 'GET' });
+ },
+
+ post(url, data, options = {}) {
+ return this.request(url, {
+ ...options,
+ method: 'POST',
+ body: JSON.stringify(data)
+ });
+ },
+
+ put(url, data, options = {}) {
+ return this.request(url, {
+ ...options,
+ method: 'PUT',
+ body: JSON.stringify(data)
+ });
+ },
+
+ delete(url, options = {}) {
+ return this.request(url, { ...options, method: 'DELETE' });
+ }
+ };
+
+ // ===== UNIFIED NOTIFICATION SYSTEM =====
+ const notifications = {
+ container: null,
+ queue: [],
+
+ init() {
+ if (this.container) return;
+
+ this.container = dom.create('div', {
+ id: 'myp-notifications',
+ class: 'fixed top-4 right-4 z-50 space-y-2'
+ });
+ document.body.appendChild(this.container);
+ },
+
+ show(message, type = 'info', duration = config.notificationDuration) {
+ this.init();
+
+ const notification = dom.create('div', {
+ class: `notification notification-${type} glass-navbar p-4 rounded-lg shadow-lg transform translate-x-full transition-transform duration-300`,
+ role: 'alert'
+ });
+
+ const content = dom.create('div', {
+ class: 'flex items-center space-x-3'
+ });
+
+ // Icon
+ const icon = dom.create('i', {
+ class: `fas ${this.getIcon(type)} text-lg`
+ });
+
+ // Message
+ const text = dom.create('span', {
+ class: 'flex-1'
+ }, message);
+
+ // Close button
+ const close = dom.create('button', {
+ class: 'ml-4 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200',
+ 'aria-label': 'Close'
+ });
+ close.innerHTML = '×';
+ close.onclick = () => this.remove(notification);
+
+ content.appendChild(icon);
+ content.appendChild(text);
+ content.appendChild(close);
+ notification.appendChild(content);
+
+ this.container.appendChild(notification);
+
+ // Animate in
+ requestAnimationFrame(() => {
+ notification.classList.remove('translate-x-full');
+ });
+
+ // Auto remove
+ if (duration > 0) {
+ setTimeout(() => this.remove(notification), duration);
+ }
+
+ return notification;
+ },
+
+ remove(notification) {
+ notification.classList.add('translate-x-full');
+ setTimeout(() => {
+ notification.remove();
+ }, 300);
+ },
+
+ getIcon(type) {
+ const icons = {
+ success: 'fa-check-circle text-green-500',
+ error: 'fa-exclamation-circle text-red-500',
+ warning: 'fa-exclamation-triangle text-yellow-500',
+ info: 'fa-info-circle text-blue-500'
+ };
+ return icons[type] || icons.info;
+ },
+
+ success(message, duration) {
+ return this.show(message, 'success', duration);
+ },
+
+ error(message, duration) {
+ return this.show(message, 'error', duration);
+ },
+
+ warning(message, duration) {
+ return this.show(message, 'warning', duration);
+ },
+
+ info(message, duration) {
+ return this.show(message, 'info', duration);
+ }
+ };
+
+ // ===== PERFORMANCE UTILITIES =====
+ const performance = {
+ debounce(func, delay = config.debounceDelay) {
+ let timeoutId;
+ return function(...args) {
+ clearTimeout(timeoutId);
+ timeoutId = setTimeout(() => func.apply(this, args), delay);
+ };
+ },
+
+ throttle(func, delay = config.throttleDelay) {
+ let lastCall = 0;
+ return function(...args) {
+ const now = Date.now();
+ if (now - lastCall >= delay) {
+ lastCall = now;
+ return func.apply(this, args);
+ }
+ };
+ },
+
+ memoize(func) {
+ const cache = new Map();
+ return function(...args) {
+ const key = JSON.stringify(args);
+ if (cache.has(key)) {
+ return cache.get(key);
+ }
+ const result = func.apply(this, args);
+ cache.set(key, result);
+ return result;
+ };
+ },
+
+ lazy(selector, callback) {
+ const observer = new IntersectionObserver((entries) => {
+ entries.forEach(entry => {
+ if (entry.isIntersecting) {
+ callback(entry.target);
+ observer.unobserve(entry.target);
+ }
+ });
+ });
+
+ dom.getAll(selector).forEach(el => observer.observe(el));
+ return observer;
+ }
+ };
+
+ // ===== STORAGE UTILITIES =====
+ const storage = {
+ set(key, value, expiry = null) {
+ const data = {
+ value,
+ timestamp: Date.now(),
+ expiry
+ };
+ try {
+ localStorage.setItem(`myp_${key}`, JSON.stringify(data));
+ return true;
+ } catch (e) {
+ console.error('Storage error:', e);
+ return false;
+ }
+ },
+
+ get(key) {
+ try {
+ const item = localStorage.getItem(`myp_${key}`);
+ if (!item) return null;
+
+ const data = JSON.parse(item);
+ if (data.expiry && Date.now() - data.timestamp > data.expiry) {
+ this.remove(key);
+ return null;
+ }
+ return data.value;
+ } catch (e) {
+ console.error('Storage error:', e);
+ return null;
+ }
+ },
+
+ remove(key) {
+ localStorage.removeItem(`myp_${key}`);
+ },
+
+ clear() {
+ Object.keys(localStorage)
+ .filter(key => key.startsWith('myp_'))
+ .forEach(key => localStorage.removeItem(key));
+ }
+ };
+
+ // ===== EVENT UTILITIES =====
+ const events = {
+ listeners: new Map(),
+
+ on(element, event, handler, options = {}) {
+ const key = `${element}_${event}`;
+ if (!this.listeners.has(key)) {
+ this.listeners.set(key, new Set());
+ }
+
+ this.listeners.get(key).add(handler);
+ element.addEventListener(event, handler, options);
+ },
+
+ off(element, event, handler) {
+ const key = `${element}_${event}`;
+ if (this.listeners.has(key)) {
+ this.listeners.get(key).delete(handler);
+ if (this.listeners.get(key).size === 0) {
+ this.listeners.delete(key);
+ }
+ }
+ element.removeEventListener(event, handler);
+ },
+
+ once(element, event, handler, options = {}) {
+ const onceHandler = (e) => {
+ handler(e);
+ this.off(element, event, onceHandler);
+ };
+ this.on(element, event, onceHandler, options);
+ },
+
+ emit(name, detail = {}) {
+ const event = new CustomEvent(name, {
+ detail,
+ bubbles: true,
+ cancelable: true
+ });
+ document.dispatchEvent(event);
+ },
+
+ cleanup() {
+ this.listeners.forEach((handlers, key) => {
+ const [element, event] = key.split('_');
+ handlers.forEach(handler => {
+ element.removeEventListener(event, handler);
+ });
+ });
+ this.listeners.clear();
+ }
+ };
+
+ // ===== FORM UTILITIES =====
+ const forms = {
+ serialize(form) {
+ const data = new FormData(form);
+ const obj = {};
+ for (const [key, value] of data.entries()) {
+ if (obj[key]) {
+ if (!Array.isArray(obj[key])) {
+ obj[key] = [obj[key]];
+ }
+ obj[key].push(value);
+ } else {
+ obj[key] = value;
+ }
+ }
+ return obj;
+ },
+
+ validate(form) {
+ const inputs = form.querySelectorAll('[required]');
+ let valid = true;
+
+ inputs.forEach(input => {
+ if (!input.value.trim()) {
+ input.classList.add('border-red-500');
+ valid = false;
+ } else {
+ input.classList.remove('border-red-500');
+ }
+ });
+
+ return valid;
+ },
+
+ reset(form) {
+ form.reset();
+ form.querySelectorAll('.border-red-500').forEach(el => {
+ el.classList.remove('border-red-500');
+ });
+ }
+ };
+
+ // ===== INITIALIZATION =====
+ const init = () => {
+ // Initialize notifications
+ notifications.init();
+
+ // Clean up on page unload
+ window.addEventListener('beforeunload', () => {
+ events.cleanup();
+ dom.clearCache();
+ });
+ };
+
+ // ===== PUBLIC API =====
+ MYP.utils = {
+ csrf,
+ dom,
+ api,
+ notifications,
+ performance,
+ storage,
+ events,
+ forms,
+ init,
+ config
+ };
+
+ // Auto-initialize when DOM is ready
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', init);
+ } else {
+ init();
+ }
+
+ // Export to global scope
+ window.MYP = MYP;
+
+})(window);
\ No newline at end of file
diff --git a/backend/templates/base.html b/backend/templates/base.html
index ab41f8e1..9d630685 100644
--- a/backend/templates/base.html
+++ b/backend/templates/base.html
@@ -36,9 +36,12 @@
+
+
+
-
-
+
+
{% block extra_css %}{% endblock %}
@@ -104,9 +107,9 @@
// User-Dropdown-Funktionalität initialisieren
initUserDropdown();
- // Flask Flash Messages über das Glassmorphism-System anzeigen
+ // Flask Flash Messages über das neue Core Utilities System anzeigen
const flashContainer = document.getElementById('flask-flash-messages');
- if (flashContainer) {
+ if (flashContainer && window.MYP && window.MYP.utils) {
const flashCount = parseInt(flashContainer.getAttribute('data-flash-count')) || 0;
for (let i = 1; i <= flashCount; i++) {
@@ -118,18 +121,10 @@
// Flask-Kategorien zu JavaScript-Kategorien mappen
if (messageType === 'danger') messageType = 'error';
- // Nachricht über das moderne Glassmorphism-System anzeigen
- if (typeof showToast === 'function') {
- // Kleine Verzögerung für bessere UX und schöne Glassmorphism-Animationen
- setTimeout(() => {
- showToast(message, messageType, 6000, {
- title: messageType === 'success' ? 'Erfolgreich' :
- messageType === 'error' ? 'Fehler' :
- messageType === 'warning' ? 'Warnung' : 'Information',
- playSound: true
- });
- }, i * 250); // Nachrichten gestaffelt anzeigen
- }
+ // Nachricht über das Core Utilities System anzeigen
+ setTimeout(() => {
+ window.MYP.utils.notifications[messageType](message, 6000);
+ }, i * 100); // Reduzierte Verzögerung für schnelleres Feedback
}
}
@@ -905,9 +900,9 @@
// User-Dropdown-Funktionalität initialisieren
initUserDropdown();
- // Flask Flash Messages über das Glassmorphism-System anzeigen
+ // Flask Flash Messages über das neue Core Utilities System anzeigen
const flashContainer = document.getElementById('flask-flash-messages');
- if (flashContainer) {
+ if (flashContainer && window.MYP && window.MYP.utils) {
const flashCount = parseInt(flashContainer.getAttribute('data-flash-count')) || 0;
for (let i = 1; i <= flashCount; i++) {
@@ -919,18 +914,10 @@
// Flask-Kategorien zu JavaScript-Kategorien mappen
if (messageType === 'danger') messageType = 'error';
- // Nachricht über das moderne Glassmorphism-System anzeigen
- if (typeof showToast === 'function') {
- // Kleine Verzögerung für bessere UX und schöne Glassmorphism-Animationen
- setTimeout(() => {
- showToast(message, messageType, 6000, {
- title: messageType === 'success' ? 'Erfolgreich' :
- messageType === 'error' ? 'Fehler' :
- messageType === 'warning' ? 'Warnung' : 'Information',
- playSound: true
- });
- }, i * 250); // Nachrichten gestaffelt anzeigen
- }
+ // Nachricht über das Core Utilities System anzeigen
+ setTimeout(() => {
+ window.MYP.utils.notifications[messageType](message, 6000);
+ }, i * 100); // Reduzierte Verzögerung für schnelleres Feedback
}
}