|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const Utils = {
|
|
|
|
|
|
toast: {
|
|
|
show(message, type = "info", duration = 3000) {
|
|
|
const container = document.getElementById("toast-container");
|
|
|
const toast = document.createElement("div");
|
|
|
toast.className = `toast ${type}`;
|
|
|
|
|
|
const icons = {
|
|
|
success: "✅",
|
|
|
error: "❌",
|
|
|
warning: "⚠️",
|
|
|
info: "ℹ️",
|
|
|
loading: "⏳"
|
|
|
};
|
|
|
|
|
|
toast.innerHTML = `
|
|
|
<span class="toast-icon">${icons[type]}</span>
|
|
|
<span class="toast-message">${message}</span>
|
|
|
<button class="toast-close">×</button>
|
|
|
`;
|
|
|
|
|
|
container.appendChild(toast);
|
|
|
|
|
|
const close = () => {
|
|
|
toast.style.animation = "slideOut 0.3s ease";
|
|
|
setTimeout(() => toast.remove(), 300);
|
|
|
};
|
|
|
|
|
|
toast.querySelector(".toast-close").onclick = close;
|
|
|
if (duration > 0) setTimeout(close, duration);
|
|
|
|
|
|
return toast;
|
|
|
},
|
|
|
|
|
|
success: (msg, duration) => Utils.toast.show(msg, "success", duration),
|
|
|
error: (msg, duration) => Utils.toast.show(msg, "error", duration),
|
|
|
warning: (msg, duration) => Utils.toast.show(msg, "warning", duration),
|
|
|
info: (msg, duration) => Utils.toast.show(msg, "info", duration)
|
|
|
},
|
|
|
|
|
|
|
|
|
modal: {
|
|
|
open(modalId) {
|
|
|
const modal = document.getElementById(modalId);
|
|
|
if (modal) {
|
|
|
modal.classList.add("active");
|
|
|
document.body.style.overflow = "hidden";
|
|
|
}
|
|
|
},
|
|
|
|
|
|
close(modalId) {
|
|
|
const modal = document.getElementById(modalId);
|
|
|
if (modal) {
|
|
|
modal.classList.remove("active");
|
|
|
document.body.style.overflow = "";
|
|
|
}
|
|
|
},
|
|
|
|
|
|
init() {
|
|
|
document.querySelectorAll(".modal").forEach(modal => {
|
|
|
modal.addEventListener("click", e => {
|
|
|
if (e.target === modal) {
|
|
|
Utils.modal.close(modal.id);
|
|
|
}
|
|
|
});
|
|
|
});
|
|
|
|
|
|
document.querySelectorAll(".modal-close, [data-modal]").forEach(btn => {
|
|
|
btn.addEventListener("click", () => {
|
|
|
const modalId = btn.getAttribute("data-modal");
|
|
|
if (modalId) Utils.modal.close(modalId);
|
|
|
});
|
|
|
});
|
|
|
}
|
|
|
},
|
|
|
|
|
|
|
|
|
storage: {
|
|
|
|
|
|
_encrypt(text) {
|
|
|
const key = "ElysiaStudio2025";
|
|
|
let encrypted = "";
|
|
|
for (let i = 0; i < text.length; i++) {
|
|
|
encrypted += String.fromCharCode(text.charCodeAt(i) ^ key.charCodeAt(i % key.length));
|
|
|
}
|
|
|
return btoa(encrypted);
|
|
|
},
|
|
|
|
|
|
_decrypt(encrypted) {
|
|
|
try {
|
|
|
const decoded = atob(encrypted);
|
|
|
const key = "ElysiaStudio2025";
|
|
|
let decrypted = "";
|
|
|
for (let i = 0; i < decoded.length; i++) {
|
|
|
decrypted += String.fromCharCode(decoded.charCodeAt(i) ^ key.charCodeAt(i % key.length));
|
|
|
}
|
|
|
return decrypted;
|
|
|
} catch {
|
|
|
return null;
|
|
|
}
|
|
|
},
|
|
|
|
|
|
get(key, defaultValue = null) {
|
|
|
try {
|
|
|
const value = localStorage.getItem(key);
|
|
|
if (!value) return defaultValue;
|
|
|
|
|
|
|
|
|
if (key === "apiKey") {
|
|
|
const decrypted = this._decrypt(value);
|
|
|
return decrypted || defaultValue;
|
|
|
}
|
|
|
|
|
|
return JSON.parse(value);
|
|
|
} catch {
|
|
|
return defaultValue;
|
|
|
}
|
|
|
},
|
|
|
|
|
|
set(key, value) {
|
|
|
try {
|
|
|
|
|
|
if (key === "apiKey" && value) {
|
|
|
localStorage.setItem(key, this._encrypt(value));
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
localStorage.setItem(key, JSON.stringify(value));
|
|
|
return true;
|
|
|
} catch {
|
|
|
return false;
|
|
|
}
|
|
|
},
|
|
|
|
|
|
remove(key) {
|
|
|
localStorage.removeItem(key);
|
|
|
},
|
|
|
|
|
|
clear() {
|
|
|
localStorage.clear();
|
|
|
}
|
|
|
},
|
|
|
|
|
|
|
|
|
formatDateTime(date) {
|
|
|
const d = new Date(date);
|
|
|
return d.toLocaleString();
|
|
|
},
|
|
|
|
|
|
|
|
|
formatDate(date) {
|
|
|
const d = new Date(date);
|
|
|
const now = new Date();
|
|
|
const diff = now - d;
|
|
|
|
|
|
if (diff < 60000) return "Just now";
|
|
|
if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`;
|
|
|
if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`;
|
|
|
if (diff < 604800000) return `${Math.floor(diff / 86400000)}d ago`;
|
|
|
|
|
|
return d.toLocaleDateString();
|
|
|
},
|
|
|
|
|
|
|
|
|
countWords(text) {
|
|
|
return text.trim() ? text.trim().split(/\s+/).length : 0;
|
|
|
},
|
|
|
|
|
|
|
|
|
countChars(text) {
|
|
|
return text.length;
|
|
|
},
|
|
|
|
|
|
|
|
|
countLines(text) {
|
|
|
return text.split("\n").length;
|
|
|
},
|
|
|
|
|
|
|
|
|
readingTime(wordCount) {
|
|
|
const minutes = Math.ceil(wordCount / 200);
|
|
|
if (minutes < 1) return "< 1 min read";
|
|
|
if (minutes === 1) return "1 min read";
|
|
|
return `${minutes} min read`;
|
|
|
},
|
|
|
|
|
|
|
|
|
downloadFile(content, filename, mimeType = "text/plain") {
|
|
|
const blob = new Blob([content], { type: mimeType });
|
|
|
const url = URL.createObjectURL(blob);
|
|
|
const a = document.createElement("a");
|
|
|
a.href = url;
|
|
|
a.download = filename;
|
|
|
a.click();
|
|
|
URL.revokeObjectURL(url);
|
|
|
},
|
|
|
|
|
|
|
|
|
async copyToClipboard(text) {
|
|
|
try {
|
|
|
await navigator.clipboard.writeText(text);
|
|
|
Utils.toast.success("Copied to clipboard!");
|
|
|
return true;
|
|
|
} catch (err) {
|
|
|
Utils.toast.error("Failed to copy");
|
|
|
return false;
|
|
|
}
|
|
|
},
|
|
|
|
|
|
|
|
|
debounce(func, wait) {
|
|
|
let timeout;
|
|
|
return function executedFunction(...args) {
|
|
|
const later = () => {
|
|
|
clearTimeout(timeout);
|
|
|
func(...args);
|
|
|
};
|
|
|
clearTimeout(timeout);
|
|
|
timeout = setTimeout(later, wait);
|
|
|
};
|
|
|
},
|
|
|
|
|
|
|
|
|
uuid() {
|
|
|
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, c => {
|
|
|
const r = (Math.random() * 16) | 0;
|
|
|
const v = c === "x" ? r : (r & 0x3) | 0x8;
|
|
|
return v.toString(16);
|
|
|
});
|
|
|
},
|
|
|
|
|
|
|
|
|
sanitizeFilename(name) {
|
|
|
return name.replace(/[^a-z0-9_\-\.]/gi, "_");
|
|
|
},
|
|
|
|
|
|
|
|
|
truncate(text, maxLength) {
|
|
|
return text.length > maxLength ? text.substring(0, maxLength) + "..." : text;
|
|
|
},
|
|
|
|
|
|
|
|
|
escapeHtml(text) {
|
|
|
const div = document.createElement("div");
|
|
|
div.textContent = text;
|
|
|
return div.innerHTML;
|
|
|
}
|
|
|
};
|
|
|
|
|
|
|
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|
|
Utils.modal.init();
|
|
|
});
|
|
|
|
|
|
export default Utils;
|
|
|
|