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