updateweb
This commit is contained in:
@@ -1,143 +1,555 @@
|
||||
// Smooth Page Transitions for Sky Art Shop
|
||||
// Provides fade-out/fade-in effects when navigating between pages
|
||||
/**
|
||||
* Page Transitions and Smooth Navigation
|
||||
* Handles page loading, transitions, and history management
|
||||
*/
|
||||
|
||||
(function () {
|
||||
"use strict";
|
||||
class PageTransitions {
|
||||
constructor() {
|
||||
this.transitionDuration = 300;
|
||||
this.isTransitioning = false;
|
||||
this.init();
|
||||
}
|
||||
|
||||
// Add page transition styles (less aggressive approach)
|
||||
const style = document.createElement("style");
|
||||
style.textContent = `
|
||||
body {
|
||||
transition: opacity 0.25s ease-in-out;
|
||||
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);
|
||||
}
|
||||
|
||||
body.page-transitioning {
|
||||
|
||||
// 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";
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
setupLinkInterception() {
|
||||
document.addEventListener("click", (e) => {
|
||||
const link = e.target.closest("a");
|
||||
|
||||
// Check if it's a valid internal link
|
||||
if (!link) return;
|
||||
if (link.hasAttribute("data-no-transition")) return;
|
||||
if (link.target === "_blank") return;
|
||||
if (link.hasAttribute("download")) return;
|
||||
|
||||
const href = link.getAttribute("href");
|
||||
if (
|
||||
!href ||
|
||||
href.startsWith("#") ||
|
||||
href.startsWith("mailto:") ||
|
||||
href.startsWith("tel:")
|
||||
)
|
||||
return;
|
||||
|
||||
// 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
|
||||
e.preventDefault();
|
||||
this.navigate(href, true);
|
||||
});
|
||||
}
|
||||
|
||||
navigate(url, updateHistory = true) {
|
||||
if (this.isTransitioning) return;
|
||||
this.isTransitioning = true;
|
||||
|
||||
this.fadeOut(() => {
|
||||
if (updateHistory) {
|
||||
history.pushState({ url }, "", url);
|
||||
}
|
||||
window.location.href = url;
|
||||
});
|
||||
}
|
||||
|
||||
// Scroll to element with smooth animation
|
||||
scrollTo(selector, offset = 0) {
|
||||
const element = document.querySelector(selector);
|
||||
if (!element) return;
|
||||
|
||||
const top =
|
||||
element.getBoundingClientRect().top + window.pageYOffset - offset;
|
||||
|
||||
window.scrollTo({
|
||||
top,
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
|
||||
// Scroll to top
|
||||
scrollToTop() {
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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",
|
||||
}
|
||||
);
|
||||
|
||||
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;
|
||||
|
||||
e.preventDefault();
|
||||
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",
|
||||
});
|
||||
|
||||
// Update URL without scrolling
|
||||
history.pushState(null, "", href);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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");
|
||||
}
|
||||
});
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
pointer-events: none;
|
||||
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);
|
||||
|
||||
// Fade in page on load (if coming from a transition)
|
||||
function initPageTransition() {
|
||||
// Check if we're coming from a transition
|
||||
const isTransitioning = sessionStorage.getItem("page-transitioning");
|
||||
if (isTransitioning === "true") {
|
||||
document.body.style.opacity = "0";
|
||||
sessionStorage.removeItem("page-transitioning");
|
||||
|
||||
// Wait for content to be ready, then fade in
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
document.body.style.opacity = "1";
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Handle navigation with transitions
|
||||
function setupNavigationTransitions() {
|
||||
// Get all internal links
|
||||
document.addEventListener("click", function (e) {
|
||||
const link = e.target.closest("a");
|
||||
|
||||
if (!link) return;
|
||||
|
||||
const href = link.getAttribute("href");
|
||||
|
||||
// Skip if:
|
||||
// - External link
|
||||
// - Opens in new tab
|
||||
// - Has download attribute
|
||||
// - Is a hash link on same page
|
||||
// - Is a javascript: link
|
||||
// - Is a mailto: or tel: link
|
||||
if (
|
||||
!href ||
|
||||
link.target === "_blank" ||
|
||||
link.hasAttribute("download") ||
|
||||
href.startsWith("javascript:") ||
|
||||
href.startsWith("mailto:") ||
|
||||
href.startsWith("tel:") ||
|
||||
href.startsWith("#") ||
|
||||
(href.includes("://") && !href.includes(window.location.host))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent default navigation
|
||||
e.preventDefault();
|
||||
|
||||
// Start transition
|
||||
document.body.classList.add("page-transitioning");
|
||||
sessionStorage.setItem("page-transitioning", "true");
|
||||
|
||||
// Navigate after fade-out completes
|
||||
setTimeout(() => {
|
||||
window.location.href = href;
|
||||
}, 250); // Match CSS transition duration
|
||||
});
|
||||
}
|
||||
|
||||
// Use View Transitions API if available (Chrome 111+, Safari 18+)
|
||||
function setupViewTransitions() {
|
||||
if (!document.startViewTransition) return;
|
||||
|
||||
document.addEventListener(
|
||||
"click",
|
||||
function (e) {
|
||||
const link = e.target.closest("a");
|
||||
|
||||
if (!link) return;
|
||||
|
||||
const href = link.getAttribute("href");
|
||||
|
||||
// Same checks as above
|
||||
if (
|
||||
!href ||
|
||||
link.target === "_blank" ||
|
||||
link.hasAttribute("download") ||
|
||||
href.startsWith("javascript:") ||
|
||||
href.startsWith("mailto:") ||
|
||||
href.startsWith("tel:") ||
|
||||
href.startsWith("#") ||
|
||||
(href.includes("://") && !href.includes(window.location.host))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
// Use View Transitions API for smooth cross-page transitions
|
||||
sessionStorage.setItem("page-transitioning", "true");
|
||||
document.startViewTransition(() => {
|
||||
window.location.href = href;
|
||||
});
|
||||
},
|
||||
true
|
||||
); // Use capture to run before other handlers
|
||||
}
|
||||
|
||||
// Initialize
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
initPageTransition();
|
||||
setupNavigationTransitions();
|
||||
});
|
||||
} else {
|
||||
initPageTransition();
|
||||
setupNavigationTransitions();
|
||||
}
|
||||
|
||||
// For browsers that support View Transitions API (progressive enhancement)
|
||||
if ("startViewTransition" in document) {
|
||||
const viewStyle = document.createElement("style");
|
||||
viewStyle.textContent = `
|
||||
::view-transition-old(root),
|
||||
::view-transition-new(root) {
|
||||
animation-duration: 0.25s;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(viewStyle);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user