| |
| |
| |
| |
|
|
| |
| class Toast { |
| constructor() { |
| this.container = this.ensureContainer(); |
| } |
|
|
| ensureContainer() { |
| let container = document.getElementById("toast-container"); |
| if (!container) { |
| container = document.createElement("div"); |
| container.id = "toast-container"; |
| document.body.appendChild(container); |
| } |
| return container; |
| } |
|
|
| show(message, type = "info", duration = 3000) { |
| const toast = document.createElement("div"); |
| toast.className = `toast ${type}`; |
| toast.textContent = message; |
|
|
| this.container.appendChild(toast); |
|
|
| |
| setTimeout(() => { |
| toast.style.animation = "slideInRight 0.3s ease reverse"; |
| setTimeout(() => toast.remove(), 300); |
| }, duration); |
|
|
| return toast; |
| } |
|
|
| success(message, duration = 3000) { |
| return this.show(message, "success", duration); |
| } |
|
|
| error(message, duration = 3000) { |
| return this.show(message, "error", duration); |
| } |
|
|
| warning(message, duration = 3000) { |
| return this.show(message, "warning", duration); |
| } |
|
|
| info(message, duration = 3000) { |
| return this.show(message, "info", duration); |
| } |
| } |
|
|
| |
| class Modal { |
| constructor(title, content, options = {}) { |
| this.title = title; |
| this.content = content; |
| this.options = { |
| closeButton: true, |
| footer: true, |
| size: "medium", |
| onConfirm: null, |
| onCancel: null, |
| ...options, |
| }; |
| this.element = null; |
| this.overlay = null; |
| } |
|
|
| create() { |
| |
| this.overlay = document.createElement("div"); |
| this.overlay.className = "modal-overlay"; |
|
|
| |
| this.element = document.createElement("div"); |
| this.element.className = `modal modal-${this.options.size}`; |
|
|
| |
| const header = document.createElement("div"); |
| header.className = "modal-header"; |
| header.innerHTML = `<h2 class="modal-title">${this.title}</h2>`; |
|
|
| if (this.options.closeButton) { |
| const closeBtn = document.createElement("button"); |
| closeBtn.className = "modal-close"; |
| closeBtn.innerHTML = "×"; |
| closeBtn.onclick = () => this.close(); |
| header.appendChild(closeBtn); |
| } |
|
|
| |
| const body = document.createElement("div"); |
| body.className = "modal-body"; |
| if (typeof this.content === "string") { |
| body.innerHTML = this.content; |
| } else { |
| body.appendChild(this.content); |
| } |
|
|
| |
| let footer = ""; |
| if (this.options.footer) { |
| footer = ` |
| <div class="modal-footer"> |
| <button class="btn btn-secondary" onclick="this.closest('.modal').parentElement.nextElementSibling.click()">Cancel</button> |
| <button class="btn btn-primary" onclick="document.querySelector('.modal-confirm')?.click?.()">Confirm</button> |
| </div> |
| `; |
| } |
|
|
| this.element.appendChild(header); |
| this.element.appendChild(body); |
| if (footer) { |
| this.element.innerHTML += footer; |
| } |
|
|
| this.overlay.appendChild(this.element); |
| document.body.appendChild(this.overlay); |
|
|
| |
| this.overlay.addEventListener("click", (e) => { |
| if (e.target === this.overlay) { |
| this.close(); |
| } |
| }); |
|
|
| return this; |
| } |
|
|
| show() { |
| if (!this.element) this.create(); |
| this.overlay.classList.add("open"); |
| return this; |
| } |
|
|
| close() { |
| if (this.overlay) { |
| this.overlay.classList.remove("open"); |
| setTimeout(() => { |
| this.overlay?.remove(); |
| this.element = null; |
| this.overlay = null; |
| }, 300); |
| } |
| return this; |
| } |
|
|
| static confirm(title, message, onConfirm, onCancel) { |
| const modal = new Modal(title, `<p>${message}</p>`, { |
| footer: true, |
| onConfirm, |
| onCancel, |
| }); |
| modal.show(); |
| return modal; |
| } |
|
|
| static alert(title, message) { |
| const modal = new Modal(title, `<p>${message}</p>`, { |
| footer: false, |
| }); |
| modal.show(); |
| return modal; |
| } |
| } |
|
|
| |
| class LoadingState { |
| static setLoading(element, isLoading = true) { |
| if (!element) return; |
|
|
| if (isLoading) { |
| element.classList.add("loading"); |
| element.disabled = true; |
| const originalText = element.textContent; |
| element.setAttribute("data-original-text", originalText); |
| element.innerHTML = `<span class="spinner"></span> Loading...`; |
| } else { |
| element.classList.remove("loading"); |
| element.disabled = false; |
| const originalText = |
| element.getAttribute("data-original-text") || element.textContent; |
| element.textContent = originalText; |
| } |
| } |
|
|
| static setLoadingMultiple(elements, isLoading = true) { |
| elements.forEach((el) => this.setLoading(el, isLoading)); |
| } |
| } |
|
|
| |
| class FormValidator { |
| static validate(form) { |
| const errors = {}; |
| const inputs = form.querySelectorAll("input, textarea, select"); |
|
|
| inputs.forEach((input) => { |
| const error = this.validateInput(input); |
| if (error) { |
| errors[input.name] = error; |
| } |
| }); |
|
|
| return { |
| isValid: Object.keys(errors).length === 0, |
| errors, |
| }; |
| } |
|
|
| static validateInput(input) { |
| if (input.hasAttribute("required") && !input.value.trim()) { |
| return `${input.name || "This field"} is required`; |
| } |
|
|
| if (input.type === "email" && input.value) { |
| const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; |
| if (!emailRegex.test(input.value)) { |
| return "Please enter a valid email address"; |
| } |
| } |
|
|
| if (input.minLength && input.value.length < input.minLength) { |
| return `Minimum length is ${input.minLength} characters`; |
| } |
|
|
| if (input.maxLength && input.value.length > input.maxLength) { |
| return `Maximum length is ${input.maxLength} characters`; |
| } |
|
|
| return null; |
| } |
|
|
| static showErrors(form, errors) { |
| |
| form.querySelectorAll(".error-message").forEach((el) => el.remove()); |
|
|
| Object.entries(errors).forEach(([name, message]) => { |
| const input = form.querySelector(`[name="${name}"]`); |
| if (input) { |
| input.classList.add("error"); |
| const errorEl = document.createElement("div"); |
| errorEl.className = "error-message"; |
| errorEl.textContent = message; |
| input.parentNode.insertBefore(errorEl, input.nextSibling); |
| } |
| }); |
| } |
|
|
| static clearErrors(form) { |
| form.querySelectorAll(".error-message").forEach((el) => el.remove()); |
| form |
| .querySelectorAll(".error") |
| .forEach((el) => el.classList.remove("error")); |
| } |
| } |
|
|
| |
| class ScrollAnimation { |
| static init() { |
| const observer = new IntersectionObserver( |
| (entries) => { |
| entries.forEach((entry) => { |
| if (entry.isIntersecting) { |
| entry.target.style.opacity = "1"; |
| entry.target.style.transform = "translateY(0)"; |
| observer.unobserve(entry.target); |
| } |
| }); |
| }, |
| { |
| threshold: 0.1, |
| rootMargin: "0px 0px -50px 0px", |
| }, |
| ); |
|
|
| document.querySelectorAll(".reveal").forEach((el) => { |
| el.style.opacity = "0"; |
| el.style.transform = "translateY(20px)"; |
| el.style.transition = "opacity 0.6s ease, transform 0.6s ease"; |
| observer.observe(el); |
| }); |
| } |
| } |
|
|
| |
| class ApiClient { |
| static async request(url, options = {}) { |
| const defaultOptions = { |
| method: "GET", |
| headers: { |
| "Content-Type": "application/json", |
| }, |
| ...options, |
| }; |
|
|
| try { |
| const response = await fetch(url, defaultOptions); |
|
|
| if (!response.ok) { |
| throw new Error(`HTTP error! status: ${response.status}`); |
| } |
|
|
| return await response.json(); |
| } catch (error) { |
| console.error("API request failed:", error); |
| throw error; |
| } |
| } |
|
|
| static get(url) { |
| return this.request(url, { method: "GET" }); |
| } |
|
|
| static post(url, data) { |
| return this.request(url, { |
| method: "POST", |
| body: JSON.stringify(data), |
| }); |
| } |
|
|
| static put(url, data) { |
| return this.request(url, { |
| method: "PUT", |
| body: JSON.stringify(data), |
| }); |
| } |
|
|
| static delete(url) { |
| return this.request(url, { method: "DELETE" }); |
| } |
| } |
|
|
| |
| class PageTransition { |
| static fadeOut(element, duration = 300) { |
| return new Promise((resolve) => { |
| element.style.opacity = "0"; |
| element.style.transition = `opacity ${duration}ms ease`; |
| setTimeout(resolve, duration); |
| }); |
| } |
|
|
| static fadeIn(element, duration = 300) { |
| return new Promise((resolve) => { |
| element.style.opacity = "0"; |
| setTimeout(() => { |
| element.style.transition = `opacity ${duration}ms ease`; |
| element.style.opacity = "1"; |
| setTimeout(resolve, duration); |
| }, 10); |
| }); |
| } |
| } |
|
|
| |
| document.addEventListener("DOMContentLoaded", () => { |
| ScrollAnimation.init(); |
| }); |
|
|
| |
| window.Toast = Toast; |
| window.Modal = Modal; |
| window.LoadingState = LoadingState; |
| window.FormValidator = FormValidator; |
| window.ApiClient = ApiClient; |
| window.PageTransition = PageTransition; |
|
|
| |
| window.toast = new Toast(); |
|
|