🎉 Updated Claude settings and integrated utility scripts 🎨📚
This commit is contained in:
parent
44d89583a4
commit
7a6102f73a
@ -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": []
|
||||
}
|
||||
|
102
backend/static/css/core-utilities.css
Normal file
102
backend/static/css/core-utilities.css
Normal file
@ -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;
|
||||
}
|
||||
}
|
468
backend/static/js/core-utilities-optimized.js
Normal file
468
backend/static/js/core-utilities-optimized.js
Normal file
@ -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);
|
493
backend/static/js/core-utilities.js
Normal file
493
backend/static/js/core-utilities.js
Normal file
@ -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);
|
@ -36,9 +36,12 @@
|
||||
<link href="{{ url_for('static', filename='css/professional-theme.min.css') }}" rel="stylesheet">
|
||||
</noscript>
|
||||
|
||||
<!-- Core Utilities CSS -->
|
||||
<link href="{{ url_for('static', filename='css/core-utilities.css') }}" rel="stylesheet">
|
||||
|
||||
<!-- Preload critical resources -->
|
||||
<link rel="preload" href="{{ url_for('static', filename='js/ui-components.js') }}" as="script">
|
||||
<link rel="preload" href="{{ url_for('static', filename='js/optimization-features.js') }}" as="script">
|
||||
<link rel="preload" href="{{ url_for('static', filename='js/core-utilities.js') }}" as="script">
|
||||
<link rel="preload" href="{{ url_for('static', filename='fontawesome/css/all.min.css') }}" as="style">
|
||||
|
||||
<!-- Additional CSS -->
|
||||
{% 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
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user