Files
SkyArtShop/website/assets/js/page-transitions.js

556 lines
12 KiB
JavaScript
Raw Permalink Normal View History

2026-01-01 22:24:30 -06:00
/**
* Page Transitions and Smooth Navigation
* Handles page loading, transitions, and history management
*/
2025-12-24 00:13:23 -06:00
2026-01-01 22:24:30 -06:00
class PageTransitions {
constructor() {
this.transitionDuration = 300;
this.isTransitioning = false;
this.init();
}
2025-12-24 00:13:23 -06:00
2026-01-01 22:24:30 -06:00
init() {
// Wait for body to exist
if (!document.body) return;
// Add transition wrapper if it doesn't exist
if (!document.getElementById("page-transition")) {
const wrapper = document.createElement("div");
wrapper.id = "page-transition";
wrapper.className = "page-transition";
// Wrap main content
const main = document.querySelector("main") || document.body;
const parent = main.parentNode;
parent.insertBefore(wrapper, main);
wrapper.appendChild(main);
2025-12-24 00:13:23 -06:00
}
2026-01-01 22:24:30 -06:00
// Add fade-in on page load
this.fadeIn();
// Intercept navigation clicks
this.setupLinkInterception();
// Handle back/forward buttons
window.addEventListener("popstate", (e) => {
if (e.state && e.state.url) {
this.navigate(e.state.url, false);
}
});
// Add scroll restoration
if ("scrollRestoration" in history) {
history.scrollRestoration = "manual";
2025-12-24 00:13:23 -06:00
}
2026-01-01 22:24:30 -06:00
}
2025-12-24 00:13:23 -06:00
2026-01-01 22:24:30 -06:00
fadeIn() {
const wrapper = document.getElementById("page-transition");
if (wrapper) {
wrapper.classList.add("fade-in");
setTimeout(() => {
wrapper.classList.remove("fade-in");
}, this.transitionDuration);
}
}
fadeOut(callback) {
const wrapper = document.getElementById("page-transition");
if (wrapper) {
wrapper.classList.add("fade-out");
setTimeout(() => {
if (callback) callback();
wrapper.classList.remove("fade-out");
}, this.transitionDuration);
} else {
if (callback) callback();
2025-12-24 00:13:23 -06:00
}
}
2026-01-01 22:24:30 -06:00
setupLinkInterception() {
document.addEventListener("click", (e) => {
2025-12-24 00:13:23 -06:00
const link = e.target.closest("a");
2026-01-01 22:24:30 -06:00
// Check if it's a valid internal link
2025-12-24 00:13:23 -06:00
if (!link) return;
2026-01-01 22:24:30 -06:00
if (link.hasAttribute("data-no-transition")) return;
if (link.target === "_blank") return;
if (link.hasAttribute("download")) return;
2025-12-24 00:13:23 -06:00
const href = link.getAttribute("href");
if (
!href ||
href.startsWith("#") ||
2026-01-01 22:24:30 -06:00
href.startsWith("mailto:") ||
href.startsWith("tel:")
)
2025-12-24 00:13:23 -06:00
return;
2026-01-01 22:24:30 -06:00
// Check if it's an external link
const url = new URL(href, window.location.origin);
if (url.origin !== window.location.origin) return;
// Intercept the navigation
2025-12-24 00:13:23 -06:00
e.preventDefault();
2026-01-01 22:24:30 -06:00
this.navigate(href, true);
});
}
2025-12-24 00:13:23 -06:00
2026-01-01 22:24:30 -06:00
navigate(url, updateHistory = true) {
if (this.isTransitioning) return;
this.isTransitioning = true;
2025-12-24 00:13:23 -06:00
2026-01-01 22:24:30 -06:00
this.fadeOut(() => {
if (updateHistory) {
history.pushState({ url }, "", url);
}
window.location.href = url;
2025-12-24 00:13:23 -06:00
});
}
2026-01-01 22:24:30 -06:00
// Scroll to element with smooth animation
scrollTo(selector, offset = 0) {
const element = document.querySelector(selector);
if (!element) return;
2025-12-24 00:13:23 -06:00
2026-01-01 22:24:30 -06:00
const top =
element.getBoundingClientRect().top + window.pageYOffset - offset;
2025-12-24 00:13:23 -06:00
2026-01-01 22:24:30 -06:00
window.scrollTo({
top,
behavior: "smooth",
});
}
2025-12-24 00:13:23 -06:00
2026-01-01 22:24:30 -06:00
// Scroll to top
scrollToTop() {
window.scrollTo({
top: 0,
behavior: "smooth",
});
}
}
2025-12-24 00:13:23 -06:00
2026-01-01 22:24:30 -06:00
/**
* Lazy Loading Images
* Improves performance by loading images only when they're visible
*/
class LazyLoader {
constructor() {
this.images = [];
this.observer = null;
this.init();
}
init() {
// Find all lazy images
this.images = document.querySelectorAll(
'img[data-src], img[loading="lazy"]'
);
// Set up Intersection Observer
if ("IntersectionObserver" in window) {
this.observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
this.loadImage(entry.target);
}
});
},
{
rootMargin: "50px",
2025-12-24 00:13:23 -06:00
}
2026-01-01 22:24:30 -06:00
);
this.images.forEach((img) => this.observer.observe(img));
} else {
// Fallback for older browsers
this.images.forEach((img) => this.loadImage(img));
}
}
loadImage(img) {
const src = img.getAttribute("data-src");
if (src) {
img.src = src;
img.removeAttribute("data-src");
}
// Add fade-in effect
img.addEventListener("load", () => {
img.classList.add("loaded");
});
if (this.observer) {
this.observer.unobserve(img);
}
}
// Add new images to observer
observe(images) {
if (!images) return;
const imageList = Array.isArray(images) ? images : [images];
imageList.forEach((img) => {
if (this.observer) {
this.observer.observe(img);
} else {
this.loadImage(img);
}
});
}
}
/**
* Smooth Scroll Handler
* Adds smooth scrolling to anchor links
*/
class SmoothScroll {
constructor() {
this.init();
}
init() {
document.querySelectorAll('a[href^="#"]').forEach((anchor) => {
anchor.addEventListener("click", (e) => {
const href = anchor.getAttribute("href");
if (href === "#") return;
2025-12-24 00:13:23 -06:00
e.preventDefault();
2026-01-01 22:24:30 -06:00
const target = document.querySelector(href);
if (target) {
const offset = 80; // Account for fixed header
const top =
target.getBoundingClientRect().top + window.pageYOffset - offset;
window.scrollTo({
top,
behavior: "smooth",
});
2025-12-24 00:13:23 -06:00
2026-01-01 22:24:30 -06:00
// Update URL without scrolling
history.pushState(null, "", href);
}
});
2025-12-24 00:13:23 -06:00
});
2026-01-01 22:24:30 -06:00
}
}
/**
* Back to Top Button
* Shows/hides button based on scroll position
*/
class BackToTop {
constructor() {
this.button = null;
this.scrollThreshold = 300;
this.init();
}
init() {
// Wait for body to exist
if (!document.body) return;
// Create button if it doesn't exist
this.button = document.getElementById("back-to-top");
if (!this.button) {
this.button = document.createElement("button");
this.button.id = "back-to-top";
this.button.className = "back-to-top";
this.button.innerHTML = "↑";
this.button.setAttribute("aria-label", "Back to top");
document.body.appendChild(this.button);
}
// Handle scroll
window.addEventListener("scroll", () => {
if (window.pageYOffset > this.scrollThreshold) {
this.button.classList.add("visible");
} else {
this.button.classList.remove("visible");
2025-12-24 00:13:23 -06:00
}
2026-01-01 22:24:30 -06:00
});
// Handle click
this.button.addEventListener("click", () => {
window.scrollTo({
top: 0,
behavior: "smooth",
});
});
}
}
/**
* Loading Overlay
* Shows loading state during async operations
*/
class LoadingOverlay {
constructor() {
this.overlay = null;
this.activeOperations = 0;
this.init();
}
init() {
// Wait for body to exist
if (!document.body) return;
// Create overlay if it doesn't exist
this.overlay = document.getElementById("loading-overlay");
if (!this.overlay) {
this.overlay = document.createElement("div");
this.overlay.id = "loading-overlay";
this.overlay.className = "loading-overlay";
this.overlay.innerHTML = `
<div class="loading-spinner">
<div class="spinner"></div>
<p>Loading...</p>
</div>
`;
document.body.appendChild(this.overlay);
}
2025-12-24 00:13:23 -06:00
}
2026-01-01 22:24:30 -06:00
show() {
this.activeOperations++;
this.overlay.classList.add("active");
document.body.style.overflow = "hidden";
}
hide() {
this.activeOperations = Math.max(0, this.activeOperations - 1);
if (this.activeOperations === 0) {
this.overlay.classList.remove("active");
document.body.style.overflow = "";
}
}
// Force hide regardless of operation count
forceHide() {
this.activeOperations = 0;
this.overlay.classList.remove("active");
document.body.style.overflow = "";
}
}
/**
* Page Visibility Handler
* Handles actions when page becomes visible/hidden
*/
class PageVisibility {
constructor() {
this.callbacks = {
visible: [],
hidden: [],
};
this.init();
}
init() {
document.addEventListener("visibilitychange", () => {
if (document.hidden) {
this.callbacks.hidden.forEach((cb) => cb());
} else {
this.callbacks.visible.forEach((cb) => cb());
}
});
}
onVisible(callback) {
this.callbacks.visible.push(callback);
}
onHidden(callback) {
this.callbacks.hidden.push(callback);
}
}
/**
* Network Status Handler
* Monitors online/offline status
*/
class NetworkStatus {
constructor() {
this.isOnline = navigator.onLine;
this.callbacks = {
online: [],
offline: [],
};
this.init();
}
init() {
window.addEventListener("online", () => {
this.isOnline = true;
this.callbacks.online.forEach((cb) => cb());
this.showNotification("Back online", "success");
});
window.addEventListener("offline", () => {
this.isOnline = false;
this.callbacks.offline.forEach((cb) => cb());
this.showNotification("No internet connection", "error");
});
}
onOnline(callback) {
this.callbacks.online.push(callback);
}
onOffline(callback) {
this.callbacks.offline.push(callback);
}
showNotification(message, type) {
if (window.Utils && window.Utils.notify) {
window.Utils.notify(message, type);
}
}
}
// Initialize when DOM is ready
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", initPageTransitions);
} else {
initPageTransitions();
}
function initPageTransitions() {
// Initialize all modules
window.pageTransitions = new PageTransitions();
window.lazyLoader = new LazyLoader();
window.smoothScroll = new SmoothScroll();
window.backToTop = new BackToTop();
window.loadingOverlay = new LoadingOverlay();
window.pageVisibility = new PageVisibility();
window.networkStatus = new NetworkStatus();
console.log("Page transitions initialized");
}
// Add CSS if not already present
if (!document.getElementById("page-transitions-styles")) {
const style = document.createElement("style");
style.id = "page-transitions-styles";
style.textContent = `
.page-transition {
opacity: 1;
transition: opacity 300ms ease;
}
.page-transition.fade-in {
opacity: 0;
animation: fadeIn 300ms ease forwards;
}
.page-transition.fade-out {
opacity: 1;
animation: fadeOut 300ms ease forwards;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes fadeOut {
from { opacity: 1; }
to { opacity: 0; }
}
img[data-src] {
opacity: 0;
transition: opacity 300ms ease;
}
img.loaded {
opacity: 1;
}
.back-to-top {
position: fixed;
bottom: 20px;
right: 20px;
width: 50px;
height: 50px;
background: #667eea;
color: white;
border: none;
border-radius: 50%;
font-size: 24px;
cursor: pointer;
opacity: 0;
visibility: hidden;
transform: translateY(20px);
transition: all 0.3s ease;
z-index: 999;
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
}
.back-to-top.visible {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.back-to-top:hover {
background: #5568d3;
transform: translateY(-2px);
}
.loading-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(255,255,255,0.95);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
visibility: hidden;
transition: all 0.3s ease;
z-index: 9999;
}
.loading-overlay.active {
opacity: 1;
visibility: visible;
}
.loading-spinner {
text-align: center;
}
.spinner {
width: 60px;
height: 60px;
border: 4px solid #f3f3f3;
border-top: 4px solid #667eea;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 16px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-spinner p {
color: #667eea;
font-size: 16px;
font-weight: 600;
margin: 0;
}
`;
document.head.appendChild(style);
}