webupdate

This commit is contained in:
Local Server
2026-01-20 20:29:33 -06:00
parent f8068ba54c
commit 1b2502c38d
22 changed files with 1905 additions and 172 deletions

View File

@@ -28,6 +28,6 @@ MAX_FILE_SIZE=62914560
SMTP_HOST=smtp.gmail.com SMTP_HOST=smtp.gmail.com
SMTP_PORT=587 SMTP_PORT=587
SMTP_SECURE=false SMTP_SECURE=false
SMTP_USER=YOUR_GMAIL@gmail.com SMTP_USER=skyartshop12.11@gmail.com
SMTP_PASS=YOUR_APP_PASSWORD SMTP_PASS=YOUR_APP_PASSWORD
SMTP_FROM="Sky Art Shop" <YOUR_GMAIL@gmail.com> SMTP_FROM="Sky Art Shop" <skyartshop12.11@gmail.com>

View File

@@ -0,0 +1,36 @@
-- Migration 009: Contact Messages and Newsletter Subscribers
-- Description: Tables for contact form submissions and newsletter subscriptions (non-customer)
-- Contact Messages Table
CREATE TABLE IF NOT EXISTS contact_messages (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL,
subject VARCHAR(500) NOT NULL,
message TEXT NOT NULL,
is_read BOOLEAN DEFAULT FALSE,
is_archived BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
read_at TIMESTAMP WITH TIME ZONE,
replied_at TIMESTAMP WITH TIME ZONE
);
-- Newsletter Subscribers Table (for non-registered users)
CREATE TABLE IF NOT EXISTS newsletter_subscribers (
id SERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
is_active BOOLEAN DEFAULT TRUE,
subscribed_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
unsubscribed_at TIMESTAMP WITH TIME ZONE,
source VARCHAR(50) DEFAULT 'website' -- 'website', 'blog', 'home', 'footer'
);
-- Indexes
CREATE INDEX IF NOT EXISTS idx_contact_messages_created ON contact_messages(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_contact_messages_unread ON contact_messages(is_read) WHERE is_read = FALSE;
CREATE INDEX IF NOT EXISTS idx_newsletter_subscribers_active ON newsletter_subscribers(is_active) WHERE is_active = TRUE;
CREATE INDEX IF NOT EXISTS idx_newsletter_subscribers_email ON newsletter_subscribers(email);
-- Comments
COMMENT ON TABLE contact_messages IS 'Contact form submissions from the website';
COMMENT ON TABLE newsletter_subscribers IS 'Newsletter subscribers who are not registered customers';

View File

@@ -0,0 +1,394 @@
/**
* Contact and Newsletter Routes
* Handles contact form submissions and newsletter subscriptions
*/
const express = require("express");
const nodemailer = require("nodemailer");
const { query } = require("../config/database");
const logger = require("../config/logger");
const { asyncHandler } = require("../middleware/errorHandler");
const { sendSuccess, sendError } = require("../utils/responseHelpers");
const rateLimit = require("express-rate-limit");
const router = express.Router();
// Rate limiting for contact form (5 submissions per 15 minutes)
const contactLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5,
message: {
success: false,
message: "Too many contact requests. Please try again later.",
},
});
// Rate limiting for newsletter (10 subscriptions per hour)
const newsletterLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
max: 10,
message: {
success: false,
message: "Too many subscription attempts. Please try again later.",
},
});
// Create email transporter
function createTransporter() {
if (
!process.env.SMTP_HOST ||
!process.env.SMTP_USER ||
!process.env.SMTP_PASS
) {
logger.warn("SMTP not configured - emails will be logged only");
return null;
}
return nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: parseInt(process.env.SMTP_PORT) || 587,
secure: process.env.SMTP_SECURE === "true",
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
},
});
}
// ===========================
// CONTACT FORM SUBMISSION
// ===========================
router.post(
"/contact",
contactLimiter,
asyncHandler(async (req, res) => {
const { name, email, subject, message } = req.body;
// Validation
if (!name || !email || !subject || !message) {
return sendError(res, "All fields are required", 400);
}
// Basic email validation
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
return sendError(res, "Please provide a valid email address", 400);
}
// Sanitize inputs
const sanitizedName = name.trim().slice(0, 255);
const sanitizedEmail = email.trim().toLowerCase().slice(0, 255);
const sanitizedSubject = subject.trim().slice(0, 500);
const sanitizedMessage = message.trim().slice(0, 5000);
try {
// Save to database
await query(
`INSERT INTO contact_messages (name, email, subject, message)
VALUES ($1, $2, $3, $4)`,
[sanitizedName, sanitizedEmail, sanitizedSubject, sanitizedMessage],
);
logger.info(`Contact form submission from ${sanitizedEmail}`);
// Send email notification to admin
const transporter = createTransporter();
if (transporter) {
try {
// Email to shop owner
await transporter.sendMail({
from:
process.env.SMTP_FROM ||
`"Sky Art Shop" <${process.env.SMTP_USER}>`,
to: process.env.SMTP_USER, // Send to the shop's email
replyTo: sanitizedEmail,
subject: `📬 New Contact Form Message: ${sanitizedSubject}`,
html: `
<div style="font-family: 'Segoe UI', Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 30px; background: linear-gradient(135deg, #FFEBEB 0%, #FFD0D0 100%); border-radius: 20px;">
<div style="text-align: center; margin-bottom: 20px;">
<h1 style="color: #202023; margin: 0;">📬 New Contact Message</h1>
</div>
<div style="background: white; border-radius: 16px; padding: 30px;">
<p style="color: #202023; font-size: 16px; margin-bottom: 10px;">
<strong>From:</strong> ${sanitizedName}
</p>
<p style="color: #202023; font-size: 16px; margin-bottom: 10px;">
<strong>Email:</strong> <a href="mailto:${sanitizedEmail}">${sanitizedEmail}</a>
</p>
<p style="color: #202023; font-size: 16px; margin-bottom: 10px;">
<strong>Subject:</strong> ${sanitizedSubject}
</p>
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">
<p style="color: #202023; font-size: 16px; margin-bottom: 10px;">
<strong>Message:</strong>
</p>
<div style="background: #f9f9f9; padding: 20px; border-radius: 10px; color: #333; line-height: 1.6;">
${sanitizedMessage.replace(/\n/g, "<br>")}
</div>
</div>
<p style="text-align: center; color: #666; font-size: 12px; margin-top: 20px;">
© ${new Date().getFullYear()} Sky Art Shop - Contact Form Notification
</p>
</div>
`,
});
// Auto-reply to customer
await transporter.sendMail({
from:
process.env.SMTP_FROM ||
`"Sky Art Shop" <${process.env.SMTP_USER}>`,
to: sanitizedEmail,
subject: `Thank you for contacting Sky Art Shop! 🎨`,
html: `
<div style="font-family: 'Segoe UI', Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 30px; background: linear-gradient(135deg, #FFEBEB 0%, #FFD0D0 100%); border-radius: 20px;">
<div style="text-align: center; margin-bottom: 20px;">
<h1 style="color: #202023; margin: 0;">🎨 Thank You!</h1>
</div>
<div style="background: white; border-radius: 16px; padding: 30px;">
<p style="color: #202023; font-size: 16px;">
Hi ${sanitizedName},
</p>
<p style="color: #202023; font-size: 16px; line-height: 1.6;">
Thank you for reaching out to Sky Art Shop! We've received your message and will get back to you within 24-48 hours.
</p>
<p style="color: #202023; font-size: 16px; line-height: 1.6;">
Here's a copy of your message:
</p>
<div style="background: #f9f9f9; padding: 20px; border-radius: 10px; margin: 20px 0;">
<p style="color: #666; font-size: 14px; margin: 0 0 10px 0;"><strong>Subject:</strong> ${sanitizedSubject}</p>
<p style="color: #333; font-size: 14px; margin: 0; line-height: 1.6;">${sanitizedMessage.replace(/\n/g, "<br>")}</p>
</div>
<p style="color: #202023; font-size: 16px;">
In the meantime, feel free to explore our collection at <a href="https://skyartshop.com/shop" style="color: #FCB1D8;">our shop</a>!
</p>
<p style="color: #202023; font-size: 16px;">
Best regards,<br>
The Sky Art Shop Team
</p>
</div>
<p style="text-align: center; color: #666; font-size: 12px; margin-top: 20px;">
© ${new Date().getFullYear()} Sky Art Shop
</p>
</div>
`,
});
logger.info(
`Contact notification and auto-reply sent for ${sanitizedEmail}`,
);
} catch (emailError) {
logger.error("Failed to send contact email:", emailError);
// Don't fail the request - message is saved to database
}
} else {
logger.info(
`Contact form (no SMTP): From: ${sanitizedName} <${sanitizedEmail}>, Subject: ${sanitizedSubject}`,
);
}
sendSuccess(res, {
message:
"Thank you for your message! We'll get back to you within 24-48 hours.",
});
} catch (error) {
logger.error("Contact form error:", error);
sendError(res, "Failed to send message. Please try again later.", 500);
}
}),
);
// ===========================
// NEWSLETTER SUBSCRIPTION
// ===========================
router.post(
"/newsletter/subscribe",
newsletterLimiter,
asyncHandler(async (req, res) => {
const { email, source = "website" } = req.body;
// Validation
if (!email) {
return sendError(res, "Email address is required", 400);
}
// Basic email validation
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
return sendError(res, "Please provide a valid email address", 400);
}
const sanitizedEmail = email.trim().toLowerCase().slice(0, 255);
const sanitizedSource = (source || "website").slice(0, 50);
try {
// Check if already subscribed (either in newsletter_subscribers or customers table)
const existingSubscriber = await query(
`SELECT id, is_active FROM newsletter_subscribers WHERE email = $1`,
[sanitizedEmail],
);
if (existingSubscriber.rows.length > 0) {
const subscriber = existingSubscriber.rows[0];
if (subscriber.is_active) {
return sendSuccess(res, {
message: "You're already subscribed to our newsletter!",
alreadySubscribed: true,
});
} else {
// Reactivate subscription
await query(
`UPDATE newsletter_subscribers
SET is_active = TRUE, subscribed_at = CURRENT_TIMESTAMP, unsubscribed_at = NULL, source = $1
WHERE email = $2`,
[sanitizedSource, sanitizedEmail],
);
logger.info(`Newsletter resubscription: ${sanitizedEmail}`);
}
} else {
// Check if they're a registered customer
const existingCustomer = await query(
`SELECT id, newsletter_subscribed FROM customers WHERE email = $1`,
[sanitizedEmail],
);
if (existingCustomer.rows.length > 0) {
if (existingCustomer.rows[0].newsletter_subscribed) {
return sendSuccess(res, {
message: "You're already subscribed to our newsletter!",
alreadySubscribed: true,
});
} else {
// Update customer's newsletter preference
await query(
`UPDATE customers SET newsletter_subscribed = TRUE WHERE email = $1`,
[sanitizedEmail],
);
logger.info(
`Customer newsletter subscription updated: ${sanitizedEmail}`,
);
}
} else {
// New subscriber - add to newsletter_subscribers table
await query(
`INSERT INTO newsletter_subscribers (email, source) VALUES ($1, $2)`,
[sanitizedEmail, sanitizedSource],
);
logger.info(
`New newsletter subscription: ${sanitizedEmail} (source: ${sanitizedSource})`,
);
}
}
// Send welcome email
const transporter = createTransporter();
if (transporter) {
try {
await transporter.sendMail({
from:
process.env.SMTP_FROM ||
`"Sky Art Shop" <${process.env.SMTP_USER}>`,
to: sanitizedEmail,
subject: `Welcome to Sky Art Shop Newsletter! 🎨✨`,
html: `
<div style="font-family: 'Segoe UI', Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 30px; background: linear-gradient(135deg, #FFEBEB 0%, #FFD0D0 100%); border-radius: 20px;">
<div style="text-align: center; margin-bottom: 20px;">
<h1 style="color: #202023; margin: 0;">🎨 Welcome to Sky Art Shop!</h1>
</div>
<div style="background: white; border-radius: 16px; padding: 30px;">
<p style="color: #202023; font-size: 16px; line-height: 1.6;">
Thank you for subscribing to our newsletter! You're now part of our creative community.
</p>
<p style="color: #202023; font-size: 16px; line-height: 1.6;">
Here's what you can expect:
</p>
<ul style="color: #202023; font-size: 16px; line-height: 1.8;">
<li>🛍️ Exclusive deals and discounts</li>
<li>✨ New product announcements</li>
<li>💡 Creative tips and inspiration</li>
<li>🎁 Special subscriber-only offers</li>
</ul>
<div style="text-align: center; margin-top: 30px;">
<a href="https://skyartshop.com/shop" style="display: inline-block; background: linear-gradient(135deg, #FCB1D8 0%, #F6CCDE 100%); color: #202023; text-decoration: none; padding: 15px 40px; border-radius: 50px; font-weight: 600;">
Start Shopping
</a>
</div>
</div>
<p style="text-align: center; color: #666; font-size: 12px; margin-top: 20px;">
© ${new Date().getFullYear()} Sky Art Shop<br>
<a href="https://skyartshop.com/unsubscribe?email=${encodeURIComponent(sanitizedEmail)}" style="color: #666;">Unsubscribe</a>
</p>
</div>
`,
});
logger.info(`Newsletter welcome email sent to ${sanitizedEmail}`);
} catch (emailError) {
logger.error("Failed to send newsletter welcome email:", emailError);
}
}
sendSuccess(res, {
message:
"Successfully subscribed! Check your email for a welcome message.",
});
} catch (error) {
logger.error("Newsletter subscription error:", error);
if (error.code === "23505") {
// Unique constraint violation
return sendSuccess(res, {
message: "You're already subscribed to our newsletter!",
alreadySubscribed: true,
});
}
sendError(res, "Failed to subscribe. Please try again later.", 500);
}
}),
);
// ===========================
// NEWSLETTER UNSUBSCRIBE
// ===========================
router.post(
"/newsletter/unsubscribe",
asyncHandler(async (req, res) => {
const { email } = req.body;
if (!email) {
return sendError(res, "Email address is required", 400);
}
const sanitizedEmail = email.trim().toLowerCase();
try {
// Update newsletter_subscribers table
await query(
`UPDATE newsletter_subscribers
SET is_active = FALSE, unsubscribed_at = CURRENT_TIMESTAMP
WHERE email = $1`,
[sanitizedEmail],
);
// Also update customers table if they're a customer
await query(
`UPDATE customers SET newsletter_subscribed = FALSE WHERE email = $1`,
[sanitizedEmail],
);
logger.info(`Newsletter unsubscription: ${sanitizedEmail}`);
sendSuccess(res, {
message: "You've been unsubscribed from our newsletter.",
});
} catch (error) {
logger.error("Newsletter unsubscribe error:", error);
sendError(res, "Failed to unsubscribe. Please try again later.", 500);
}
}),
);
module.exports = router;

View File

@@ -257,6 +257,7 @@ const usersRoutes = require("./routes/users");
const uploadRoutes = require("./routes/upload"); const uploadRoutes = require("./routes/upload");
const customerAuthRoutes = require("./routes/customer-auth"); const customerAuthRoutes = require("./routes/customer-auth");
const customerCartRoutes = require("./routes/customer-cart"); const customerCartRoutes = require("./routes/customer-cart");
const contactNewsletterRoutes = require("./routes/contact-newsletter");
// Admin redirect - handle /admin to redirect to login (must be before static files) // Admin redirect - handle /admin to redirect to login (must be before static files)
app.get("/admin", (req, res) => { app.get("/admin", (req, res) => {
@@ -330,6 +331,7 @@ app.use("/api/admin/users", usersRoutes);
app.use("/api/admin", uploadRoutes); app.use("/api/admin", uploadRoutes);
app.use("/api/customers", customerAuthRoutes); app.use("/api/customers", customerAuthRoutes);
app.use("/api/customers", customerCartRoutes); app.use("/api/customers", customerCartRoutes);
app.use("/api", contactNewsletterRoutes);
app.use("/api", publicRoutes); app.use("/api", publicRoutes);
// Admin static files (must be after URL rewriting) // Admin static files (must be after URL rewriting)

View File

@@ -69,6 +69,126 @@
.stat-item i { .stat-item i {
font-size: 1rem; font-size: 1rem;
} }
/* Skeleton Loading Animation */
.skeleton-item {
background: #f8fafc;
border-radius: 12px;
overflow: hidden;
pointer-events: none;
}
.skeleton-preview {
width: 100%;
aspect-ratio: 1;
background: linear-gradient(
90deg,
#f0f0f0 25%,
#e0e0e0 50%,
#f0f0f0 75%
);
background-size: 200% 100%;
animation: skeleton-shimmer 1.5s infinite;
}
.skeleton-info {
padding: 12px;
}
.skeleton-name {
height: 14px;
background: linear-gradient(
90deg,
#f0f0f0 25%,
#e0e0e0 50%,
#f0f0f0 75%
);
background-size: 200% 100%;
animation: skeleton-shimmer 1.5s infinite;
border-radius: 4px;
margin-bottom: 8px;
width: 80%;
}
.skeleton-meta {
height: 12px;
background: linear-gradient(
90deg,
#f0f0f0 25%,
#e0e0e0 50%,
#f0f0f0 75%
);
background-size: 200% 100%;
animation: skeleton-shimmer 1.5s infinite;
border-radius: 4px;
width: 50%;
}
@keyframes skeleton-shimmer {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
/* Lazy loading image placeholder - instant load, no delay */
.media-item .item-preview img {
opacity: 1;
}
.media-item .item-preview img.loaded {
opacity: 1;
}
.media-item .item-preview {
background: #f0f0f0;
display: flex;
align-items: center;
justify-content: center;
}
/* Loading indicator at bottom */
.load-more-indicator {
text-align: center;
padding: 20px;
color: #666;
display: none;
}
.load-more-indicator.visible {
display: block;
}
/* Fixed toolbar styles when scrolling */
.media-library-page {
position: relative;
}
.media-library-toolbar {
background: white;
transition: box-shadow 0.2s ease;
}
.media-library-toolbar.fixed-toolbar {
position: fixed;
top: 0;
z-index: 1000;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
border-radius: 0 !important;
border-top-left-radius: 0 !important;
border-top-right-radius: 0 !important;
}
/* Placeholder to prevent content jump when toolbar becomes fixed */
.toolbar-placeholder {
display: none;
}
.toolbar-placeholder.visible {
display: block;
}
</style> </style>
</head> </head>
<body> <body>
@@ -162,6 +282,14 @@
> >
<i class="bi bi-folder-plus"></i> New Folder <i class="bi bi-folder-plus"></i> New Folder
</button> </button>
<button
type="button"
class="btn btn-outline-danger"
id="deleteSelectedBtnTop"
style="display: none"
>
<i class="bi bi-trash"></i> Delete Selected
</button>
<div class="divider"></div> <div class="divider"></div>
<div class="search-box"> <div class="search-box">
<i class="bi bi-search"></i> <i class="bi bi-search"></i>
@@ -430,14 +558,105 @@
let sortBy = localStorage.getItem("mediaLibrarySort") || "date"; let sortBy = localStorage.getItem("mediaLibrarySort") || "date";
let searchQuery = ""; let searchQuery = "";
let draggedItem = null; let draggedItem = null;
let isLoading = false;
// Initialize // Initialize
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
initViewMode(); initViewMode();
showSkeletonLoading();
loadContent(); loadContent();
bindEvents(); bindEvents();
setupStickyToolbar();
}); });
function setupStickyToolbar() {
const toolbar = document.querySelector(".media-library-toolbar");
const mediaLibraryPage = document.querySelector(".media-library-page");
if (!toolbar || !mediaLibraryPage) return;
// Create placeholder element to prevent content jump
const placeholder = document.createElement("div");
placeholder.className = "toolbar-placeholder";
toolbar.parentNode.insertBefore(placeholder, toolbar.nextSibling);
// Store original toolbar dimensions
const toolbarHeight = toolbar.offsetHeight;
placeholder.style.height = toolbarHeight + "px";
// Get the sidebar width for proper left offset
const sidebarWidth = 250; // matches --sidebar-width in CSS
const mainContentPadding = 30; // matches main-content padding
function handleScroll() {
const pageRect = mediaLibraryPage.getBoundingClientRect();
const pageTop = pageRect.top;
// When the media-library-page scrolls past the top, fix the toolbar
if (pageTop <= 0) {
if (!toolbar.classList.contains("fixed-toolbar")) {
toolbar.classList.add("fixed-toolbar");
placeholder.classList.add("visible");
// Set the width and left position
toolbar.style.left = sidebarWidth + mainContentPadding + "px";
toolbar.style.right = mainContentPadding + "px";
}
} else {
if (toolbar.classList.contains("fixed-toolbar")) {
toolbar.classList.remove("fixed-toolbar");
placeholder.classList.remove("visible");
toolbar.style.left = "";
toolbar.style.right = "";
}
}
}
window.addEventListener("scroll", handleScroll, { passive: true });
// Initial check
handleScroll();
}
function showSkeletonLoading() {
const content = document.getElementById("mediaContent");
let skeletonHtml = "";
for (let i = 0; i < 12; i++) {
skeletonHtml += `
<div class="media-item skeleton-item">
<div class="skeleton-preview"></div>
<div class="skeleton-info">
<div class="skeleton-name"></div>
<div class="skeleton-meta"></div>
</div>
</div>
`;
}
content.innerHTML = skeletonHtml;
}
function getFilteredFolders() {
let filteredFolders = folders.filter((f) =>
currentFolder ? f.parentId === currentFolder : f.parentId === null,
);
if (searchQuery) {
filteredFolders = filteredFolders.filter((f) =>
f.name.toLowerCase().includes(searchQuery),
);
}
return filteredFolders;
}
function getFilteredFiles() {
let filteredFiles = files;
if (searchQuery) {
filteredFiles = filteredFiles.filter(
(f) =>
f.originalName.toLowerCase().includes(searchQuery) ||
f.filename.toLowerCase().includes(searchQuery),
);
}
return filteredFiles;
}
function initViewMode() { function initViewMode() {
const content = document.getElementById("mediaContent"); const content = document.getElementById("mediaContent");
content.classList.remove("grid-view", "list-view"); content.classList.remove("grid-view", "list-view");
@@ -538,11 +757,16 @@
if (e.key === "Enter") confirmRename(); if (e.key === "Enter") confirmRename();
}); });
// Delete selected // Delete selected (bottom button)
document document
.getElementById("deleteSelectedBtn") .getElementById("deleteSelectedBtn")
.addEventListener("click", deleteSelected); .addEventListener("click", deleteSelected);
// Delete selected (top button)
document
.getElementById("deleteSelectedBtnTop")
.addEventListener("click", deleteSelected);
// Back button // Back button
document document
.getElementById("backBtn") .getElementById("backBtn")
@@ -569,7 +793,11 @@
} }
async function loadContent() { async function loadContent() {
isLoading = true;
displayedCount = 0;
try { try {
// Load folders and files in parallel
const [foldersRes, filesRes] = await Promise.all([ const [foldersRes, filesRes] = await Promise.all([
fetch("/api/admin/folders", { credentials: "include" }), fetch("/api/admin/folders", { credentials: "include" }),
fetch( fetch(
@@ -588,22 +816,34 @@
folders = foldersData.folders || []; folders = foldersData.folders || [];
files = filesData.files || []; files = filesData.files || [];
updateStats(); // Render content immediately (first batch)
renderContent(); renderContent();
updateBreadcrumb(); updateBreadcrumb();
// Update stats in background (non-blocking)
updateStatsAsync();
} catch (error) { } catch (error) {
console.error("Error loading media:", error); console.error("Error loading media:", error);
showToast("Failed to load media", "error"); showToast("Failed to load media", "error");
document.getElementById("mediaContent").innerHTML = `
<div class="empty-state">
<i class="bi bi-exclamation-circle"></i>
<h5>Failed to load media</h5>
<p>Please try refreshing the page</p>
</div>
`;
} finally {
isLoading = false;
} }
} }
function updateStats() { function updateStatsAsync() {
// Count all folders // Update folder count immediately from current data
document.getElementById("folderCount").textContent = `${ document.getElementById("folderCount").textContent = `${
folders.length folders.length
} folder${folders.length !== 1 ? "s" : ""}`; } folder${folders.length !== 1 ? "s" : ""}`;
// Need to get total file count // Fetch total file stats in background
fetch("/api/admin/uploads", { credentials: "include" }) fetch("/api/admin/uploads", { credentials: "include" })
.then((res) => res.json()) .then((res) => res.json())
.then((data) => { .then((data) => {
@@ -618,32 +858,18 @@
); );
document.getElementById("totalSize").textContent = document.getElementById("totalSize").textContent =
formatFileSize(totalBytes) + " used"; formatFileSize(totalBytes) + " used";
})
.catch(() => {
// Silently fail - stats are not critical
}); });
} }
function renderContent() { function renderContent() {
const content = document.getElementById("mediaContent"); const content = document.getElementById("mediaContent");
// Filter folders for current directory // Get filtered content
let filteredFolders = folders.filter((f) => const filteredFolders = getFilteredFolders();
currentFolder ? f.parentId === currentFolder : f.parentId === null, const filteredFiles = sortFiles(getFilteredFiles());
);
let filteredFiles = files;
// Apply search
if (searchQuery) {
filteredFolders = filteredFolders.filter((f) =>
f.name.toLowerCase().includes(searchQuery),
);
filteredFiles = filteredFiles.filter(
(f) =>
f.originalName.toLowerCase().includes(searchQuery) ||
f.filename.toLowerCase().includes(searchQuery),
);
}
// Sort files
filteredFiles = sortFiles(filteredFiles);
if (filteredFolders.length === 0 && filteredFiles.length === 0) { if (filteredFolders.length === 0 && filteredFiles.length === 0) {
content.innerHTML = ` content.innerHTML = `
@@ -671,9 +897,7 @@
); );
html += ` html += `
<div class="media-item folder-item ${isSelected ? "selected" : ""}" <div class="media-item folder-item ${isSelected ? "selected" : ""}"
data-type="folder" data-id="${ data-type="folder" data-id="${folder.id}" data-name="${escapeHtml(folder.name)}"
folder.id
}" data-name="${escapeHtml(folder.name)}"
draggable="true"> draggable="true">
<div class="item-checkbox"> <div class="item-checkbox">
<input type="checkbox" ${isSelected ? "checked" : ""}> <input type="checkbox" ${isSelected ? "checked" : ""}>
@@ -697,7 +921,7 @@
`; `;
}); });
// Files // Files - use data-src for lazy loading
filteredFiles.forEach((file) => { filteredFiles.forEach((file) => {
const isSelected = selectedItems.some( const isSelected = selectedItems.some(
(s) => s.type === "file" && s.id === file.id, (s) => s.type === "file" && s.id === file.id,
@@ -705,22 +929,17 @@
html += ` html += `
<div class="media-item file-item ${isSelected ? "selected" : ""}" <div class="media-item file-item ${isSelected ? "selected" : ""}"
data-type="file" data-id="${file.id}" data-path="${file.path}" data-type="file" data-id="${file.id}" data-path="${file.path}"
data-name="${escapeHtml(file.originalName)}" data-size="${ data-name="${escapeHtml(file.originalName)}" data-size="${file.size}"
file.size
}"
draggable="true"> draggable="true">
<div class="item-checkbox"> <div class="item-checkbox">
<input type="checkbox" ${isSelected ? "checked" : ""}> <input type="checkbox" ${isSelected ? "checked" : ""}>
</div> </div>
<div class="item-preview"> <div class="item-preview">
<img src="${file.path}" alt="${escapeHtml( <img data-src="${file.path}" alt="${escapeHtml(file.originalName)}"
file.originalName, src="data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=">
)}" loading="lazy">
</div> </div>
<div class="item-info"> <div class="item-info">
<span class="item-name" title="${escapeHtml( <span class="item-name" title="${escapeHtml(file.originalName)}">${escapeHtml(file.originalName)}</span>
file.originalName,
)}">${escapeHtml(file.originalName)}</span>
<span class="item-meta">${formatFileSize(file.size)}</span> <span class="item-meta">${formatFileSize(file.size)}</span>
</div> </div>
<div class="item-actions"> <div class="item-actions">
@@ -740,6 +959,61 @@
content.innerHTML = html; content.innerHTML = html;
bindItemEvents(); bindItemEvents();
// Setup lazy loading for images
setupLazyLoading();
}
function setupLazyLoading() {
const PRELOAD_COUNT = 50; // Preload first 50 images immediately
const images = document.querySelectorAll(".item-preview img[data-src]");
// Preload first 50 images immediately
images.forEach((img, index) => {
if (index < PRELOAD_COUNT) {
const src = img.dataset.src;
if (src) {
img.src = src;
img.classList.add("loaded");
img.onerror = () => {
img.src =
'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100"><rect fill="%23f0f0f0" width="100" height="100"/><text x="50%" y="50%" text-anchor="middle" dy=".3em" fill="%23999" font-size="12">No preview</text></svg>';
};
}
}
});
// Use Intersection Observer for lazy loading remaining images with large margin
const imageObserver = new IntersectionObserver(
(entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const img = entry.target;
const src = img.dataset.src;
if (src) {
img.src = src;
img.classList.add("loaded");
img.onerror = () => {
img.src =
'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100"><rect fill="%23f0f0f0" width="100" height="100"/><text x="50%" y="50%" text-anchor="middle" dy=".3em" fill="%23999" font-size="12">No preview</text></svg>';
};
observer.unobserve(img);
}
}
});
},
{
rootMargin: "500px", // Load images 500px before they appear
threshold: 0,
},
);
// Only observe images after the first 50
images.forEach((img, index) => {
if (index >= PRELOAD_COUNT && !img.classList.contains("loaded")) {
imageObserver.observe(img);
}
});
} }
function bindItemEvents() { function bindItemEvents() {
@@ -859,14 +1133,17 @@
const count = selectedItems.length; const count = selectedItems.length;
const countEl = document.getElementById("selectedCount"); const countEl = document.getElementById("selectedCount");
const deleteBtn = document.getElementById("deleteSelectedBtn"); const deleteBtn = document.getElementById("deleteSelectedBtn");
const deleteBtnTop = document.getElementById("deleteSelectedBtnTop");
if (count > 0) { if (count > 0) {
countEl.style.display = "inline-flex"; countEl.style.display = "inline-flex";
countEl.querySelector(".count").textContent = count; countEl.querySelector(".count").textContent = count;
deleteBtn.style.display = "inline-flex"; deleteBtn.style.display = "inline-flex";
deleteBtnTop.style.display = "inline-flex";
} else { } else {
countEl.style.display = "none"; countEl.style.display = "none";
deleteBtn.style.display = "none"; deleteBtn.style.display = "none";
deleteBtnTop.style.display = "none";
} }
} }

View File

@@ -25,10 +25,10 @@
/> />
<!-- Modern Theme CSS --> <!-- Modern Theme CSS -->
<link rel="stylesheet" href="/assets/css/modern-theme.css?v=fix33" /> <link rel="stylesheet" href="/assets/css/modern-theme.css?v=fix35" />
<link <link
rel="stylesheet" rel="stylesheet"
href="/assets/css/mobile-fixes.css?v=20260118fix30" href="/assets/css/mobile-fixes.css?v=20260120fix2"
/> />
<style> <style>
@@ -608,7 +608,7 @@
</div> </div>
<div class="footer-bottom"> <div class="footer-bottom">
<p>&copy; 2026 Sky Art Shop. All rights reserved.</p> <p>&copy; 2026 PromptTech-Solution. Designed and Developed by: PromptTech-Solution</p>
<p> <p>
Made with Made with
<i class="bi bi-heart-fill" style="color: var(--primary-pink)"></i> <i class="bi bi-heart-fill" style="color: var(--primary-pink)"></i>

View File

@@ -148,34 +148,96 @@ body {
} }
} }
/* All mobile devices - Slide-in menu from right (half screen) */ /* Hide close button on desktop */
@media (max-width: 768px) { .nav-menu-close {
display: none !important;
}
/* All mobile and tablet devices - Slide-in menu from right */
@media (max-width: 1024px) {
/* Show close button on mobile/tablet */
.nav-menu-close {
display: flex !important;
}
.navbar { .navbar {
position: relative !important; position: relative !important;
} }
/* Show hamburger menu on tablets */
.nav-mobile-toggle {
display: flex !important;
flex-direction: column;
gap: 5px;
padding: 10px;
background: none;
border: none;
cursor: pointer;
z-index: 1001;
}
.nav-mobile-toggle span {
display: block;
width: 26px;
height: 3px;
background: var(--text-primary, #202023);
border-radius: 2px;
transition: all 0.3s ease;
}
/* Hamburger to X animation when active */
.nav-mobile-toggle.active span:nth-child(1) {
transform: rotate(45deg) translate(5px, 5px);
}
.nav-mobile-toggle.active span:nth-child(2) {
opacity: 0;
}
.nav-mobile-toggle.active span:nth-child(3) {
transform: rotate(-45deg) translate(7px, -6px);
}
/* Menu overlay - dark background when menu is open */
.nav-menu-overlay {
position: fixed !important;
top: 0 !important;
left: 0 !important;
right: 0 !important;
bottom: 0 !important;
background: rgba(0, 0, 0, 0.5) !important;
opacity: 0 !important;
visibility: hidden !important;
transition: opacity 0.3s ease, visibility 0.3s ease !important;
z-index: 998 !important;
}
.nav-menu-overlay.active {
opacity: 1 !important;
visibility: visible !important;
}
.nav-menu { .nav-menu {
position: fixed !important; position: fixed !important;
top: 0 !important; top: 0 !important;
right: 0 !important; right: 0 !important;
left: auto !important; left: auto !important;
bottom: 0 !important; bottom: 0 !important;
width: 50% !important; width: 320px !important;
min-width: 200px !important; max-width: 80% !important;
max-width: 300px !important;
height: 100vh !important; height: 100vh !important;
height: 100dvh !important; /* Dynamic viewport height for mobile browsers */ height: 100dvh !important; /* Dynamic viewport height for mobile browsers */
flex-direction: column !important; flex-direction: column !important;
background: #ffffff !important; background: #ffffff !important;
padding: 80px 20px 30px !important; padding: 100px 24px 30px !important;
gap: 4px !important; gap: 8px !important;
transform: translateX(100%) !important; transform: translateX(100%) !important;
opacity: 1 !important; opacity: 1 !important;
visibility: hidden !important; visibility: hidden !important;
transition: transform 0.3s ease, visibility 0.3s ease !important; transition: transform 0.3s ease, visibility 0.3s ease !important;
z-index: 999 !important; z-index: 999 !important;
overflow-y: auto !important; overflow-y: auto !important;
box-shadow: -4px 0 20px rgba(0, 0, 0, 0.1) !important; box-shadow: -4px 0 30px rgba(0, 0, 0, 0.2) !important;
box-sizing: border-box !important; box-sizing: border-box !important;
} }
@@ -184,6 +246,30 @@ body {
visibility: visible !important; visibility: visible !important;
} }
/* Close button for mobile/tablet menu */
.nav-menu-close {
position: absolute !important;
top: 24px !important;
right: 24px !important;
width: 44px !important;
height: 44px !important;
background: #f5f5f5 !important;
border: none !important;
border-radius: 50% !important;
font-size: 24px !important;
color: #333 !important;
cursor: pointer !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
transition: background 0.2s ease !important;
z-index: 1000 !important;
}
.nav-menu-close:hover {
background: #e0e0e0 !important;
}
.nav-menu li { .nav-menu li {
list-style: none !important; list-style: none !important;
margin: 0 !important; margin: 0 !important;
@@ -193,12 +279,12 @@ body {
.nav-link { .nav-link {
display: block !important; display: block !important;
width: 100% !important; width: 100% !important;
padding: 14px 16px !important; padding: 16px 20px !important;
text-align: left !important; text-align: left !important;
font-size: 1rem !important; font-size: 1.1rem !important;
font-weight: 500 !important; font-weight: 500 !important;
color: #333 !important; color: #333 !important;
border-radius: 8px !important; border-radius: 10px !important;
text-decoration: none !important; text-decoration: none !important;
transition: background 0.15s ease !important; transition: background 0.15s ease !important;
} }
@@ -215,6 +301,82 @@ body {
.nav-link::after { .nav-link::after {
display: none !important; display: none !important;
} }
/* Prevent body scroll when menu is open */
body.nav-menu-open {
overflow: hidden !important;
}
}
/* Smaller phones (max-width: 576px) - smaller menu width */
@media (max-width: 576px) {
.nav-menu {
width: 280px !important;
max-width: 85% !important;
padding: 90px 20px 30px !important;
}
.nav-link {
padding: 14px 16px !important;
font-size: 1rem !important;
}
.nav-menu-close {
top: 20px !important;
right: 20px !important;
width: 40px !important;
height: 40px !important;
font-size: 20px !important;
}
}
/* iPad and Tablets (768px - 1024px) - Larger buttons for touch */
@media (min-width: 768px) and (max-width: 1024px) {
/* Larger mobile menu for tablets */
.nav-menu {
width: 380px !important;
max-width: 70% !important;
padding: 100px 28px 40px !important;
}
/* Bigger menu links for iPad */
.nav-menu .nav-link {
padding: 20px 24px !important;
font-size: 1.25rem !important;
font-weight: 500 !important;
border-radius: 12px !important;
margin-bottom: 4px !important;
}
/* Larger close button for tablets */
.nav-menu-close {
top: 28px !important;
right: 28px !important;
width: 52px !important;
height: 52px !important;
font-size: 28px !important;
}
/* Larger hamburger button */
.nav-mobile-toggle {
padding: 12px !important;
}
.nav-mobile-toggle span {
width: 28px !important;
height: 3px !important;
}
/* Larger navbar action buttons for tablets */
.nav-icon-btn {
width: 48px !important;
height: 48px !important;
font-size: 1.4rem !important;
}
.nav-actions {
gap: 12px !important;
}
} }
/* === iPad Mini (768px) === */ /* === iPad Mini (768px) === */
@@ -442,6 +604,13 @@ body {
max-width: 100% !important; max-width: 100% !important;
overflow: hidden !important; overflow: hidden !important;
box-sizing: border-box !important; box-sizing: border-box !important;
flex: 1 !important;
display: flex !important;
flex-direction: column !important;
}
.product-footer {
margin-top: auto !important;
} }
.product-name { .product-name {
@@ -485,6 +654,18 @@ body {
overflow: hidden !important; overflow: hidden !important;
box-sizing: border-box !important; box-sizing: border-box !important;
background: #ffffff !important; background: #ffffff !important;
display: flex !important;
flex-direction: column !important;
}
.product-info {
flex: 1 !important;
display: flex !important;
flex-direction: column !important;
}
.product-footer {
margin-top: auto !important;
} }
.product-image { .product-image {
@@ -530,6 +711,18 @@ body {
min-width: 0 !important; min-width: 0 !important;
box-sizing: border-box !important; box-sizing: border-box !important;
background: #ffffff !important; background: #ffffff !important;
display: flex !important;
flex-direction: column !important;
}
.product-info {
flex: 1 !important;
display: flex !important;
flex-direction: column !important;
}
.product-footer {
margin-top: auto !important;
} }
.product-image { .product-image {
@@ -2185,13 +2378,23 @@ body {
} }
} }
/* === GLOBAL PRODUCT CARD FIX - Remove pink divider === */ /* === GLOBAL PRODUCT CARD FIX - Remove pink divider & Align footer === */
.product-card { .product-card {
display: flex !important; display: flex !important;
flex-direction: column !important; flex-direction: column !important;
background: #ffffff !important; background: #ffffff !important;
} }
.product-info {
flex: 1 !important;
display: flex !important;
flex-direction: column !important;
}
.product-footer {
margin-top: auto !important;
}
.product-image { .product-image {
background: #ffffff !important; background: #ffffff !important;
margin: 0 !important; margin: 0 !important;
@@ -2204,3 +2407,205 @@ body {
margin: 0 !important; margin: 0 !important;
padding: 0 !important; padding: 0 !important;
} }
/* ============================================
UNIVERSAL IMAGE CONTAINMENT FIXES
Ensures consistent image sizing across all devices
============================================ */
/* === SHOP PAGE: Product Grid Image Standardization === */
/* All product card images - standardized box with aspect ratio */
.products-grid .product-card {
display: flex !important;
flex-direction: column !important;
width: 100% !important;
max-width: 100% !important;
box-sizing: border-box !important;
}
.products-grid .product-image {
position: relative !important;
width: 100% !important;
max-width: 100% !important;
aspect-ratio: 1 / 1 !important;
overflow: hidden !important;
flex-shrink: 0 !important;
background: #ffffff !important;
}
.products-grid .product-image img {
position: absolute !important;
top: 0 !important;
left: 0 !important;
width: 100% !important;
height: 100% !important;
object-fit: cover !important;
object-position: center !important;
}
/* === PRODUCT DETAIL PAGE: Main Image Standardization === */
/* Desktop/Large screens */
.product-detail .main-image {
position: relative !important;
width: 100% !important;
max-width: 100% !important;
aspect-ratio: 1 / 1 !important;
overflow: hidden !important;
border-radius: var(--radius-lg) !important;
background: var(--bg-white, #ffffff) !important;
}
.product-detail .main-image img {
position: absolute !important;
top: 0 !important;
left: 0 !important;
width: 100% !important;
height: 100% !important;
object-fit: cover !important;
object-position: center !important;
}
/* Ensure gallery doesn't overflow */
.product-detail .product-gallery {
width: 100% !important;
max-width: 100% !important;
overflow: hidden !important;
box-sizing: border-box !important;
}
/* === TABLET SPECIFIC (iPad, iPad Air, iPad Pro) === */
@media (min-width: 768px) and (max-width: 1024px) {
/* Product Detail Page - Tablet */
.product-detail {
display: grid !important;
grid-template-columns: 1fr 1fr !important;
gap: var(--spacing-xl, 24px) !important;
width: 100% !important;
max-width: 100% !important;
box-sizing: border-box !important;
}
.product-detail .product-gallery {
position: relative !important;
width: 100% !important;
max-width: 100% !important;
overflow: hidden !important;
}
.product-detail .main-image {
position: relative !important;
width: 100% !important;
max-width: 100% !important;
height: 0 !important;
padding-bottom: 100% !important;
aspect-ratio: unset !important;
overflow: hidden !important;
border-radius: var(--radius-lg, 12px) !important;
background: #ffffff !important;
}
.product-detail .main-image img {
position: absolute !important;
top: 0 !important;
left: 0 !important;
width: 100% !important;
height: 100% !important;
object-fit: cover !important;
object-position: center !important;
}
.product-detail .product-details {
width: 100% !important;
max-width: 100% !important;
box-sizing: border-box !important;
overflow-wrap: break-word !important;
}
/* Container overflow prevention */
.container {
max-width: 100% !important;
overflow-x: hidden !important;
box-sizing: border-box !important;
}
}
/* === SMALL TABLET / Large Phone (600px - 768px) === */
@media (min-width: 600px) and (max-width: 767px) {
.product-detail {
display: block !important;
width: 100% !important;
max-width: 100% !important;
}
.product-detail .product-gallery {
width: 100% !important;
max-width: 100% !important;
margin-bottom: var(--spacing-lg, 16px) !important;
}
.product-detail .main-image {
position: relative !important;
width: 100% !important;
max-width: 100% !important;
height: 0 !important;
padding-bottom: 100% !important;
aspect-ratio: unset !important;
overflow: hidden !important;
border-radius: var(--radius-lg, 12px) !important;
}
.product-detail .main-image img {
position: absolute !important;
top: 0 !important;
left: 0 !important;
width: 100% !important;
height: 100% !important;
object-fit: cover !important;
object-position: center !important;
}
}
/* === MOBILE (up to 600px) === */
@media (max-width: 599px) {
.product-detail .main-image {
position: relative !important;
width: 100% !important;
max-width: 100% !important;
height: 0 !important;
padding-bottom: 100% !important;
aspect-ratio: unset !important;
overflow: hidden !important;
}
.product-detail .main-image img {
position: absolute !important;
top: 0 !important;
left: 0 !important;
width: 100% !important;
height: 100% !important;
object-fit: cover !important;
object-position: center !important;
}
}
/* === LARGE DESKTOP (1025px+) === */
@media (min-width: 1025px) {
.product-detail .main-image {
position: relative !important;
width: 100% !important;
max-width: 600px !important;
aspect-ratio: 1 / 1 !important;
overflow: hidden !important;
border-radius: var(--radius-lg, 12px) !important;
}
.product-detail .main-image img {
position: absolute !important;
top: 0 !important;
left: 0 !important;
width: 100% !important;
height: 100% !important;
object-fit: cover !important;
object-position: center !important;
}
}

View File

@@ -370,10 +370,10 @@ body {
transition: var(--transition-fast); transition: var(--transition-fast);
} }
/* Mobile Navigation - Slide from Right (Half Screen) */ /* Mobile Navigation - Slide from Right (all devices up to 1024px including iPads) */
@media (max-width: 992px) { @media (max-width: 1024px) {
.nav-mobile-toggle { .nav-mobile-toggle {
display: flex; display: flex !important;
} }
.nav-menu { .nav-menu {
@@ -381,14 +381,13 @@ body {
top: 0; top: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
width: 50%; width: 320px;
min-width: 200px; max-width: 80%;
max-width: 300px;
flex-direction: column; flex-direction: column;
background: var(--bg-white); background: var(--bg-white);
padding: 80px var(--spacing-lg) var(--spacing-xl); padding: 80px var(--spacing-lg) var(--spacing-xl);
gap: var(--spacing-xs); gap: var(--spacing-xs);
box-shadow: -4px 0 20px rgba(0, 0, 0, 0.1); box-shadow: -4px 0 20px rgba(0, 0, 0, 0.15);
transform: translateX(100%); transform: translateX(100%);
visibility: hidden; visibility: hidden;
transition: transform 0.3s ease, visibility 0.3s ease; transition: transform 0.3s ease, visibility 0.3s ease;
@@ -402,9 +401,10 @@ body {
.nav-link { .nav-link {
width: 100%; width: 100%;
padding: var(--spacing-sm) var(--spacing-md); padding: var(--spacing-md) var(--spacing-lg);
text-align: left; text-align: left;
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
font-size: 1.1rem;
} }
.nav-link:hover { .nav-link:hover {
@@ -828,6 +828,9 @@ body {
.product-info { .product-info {
padding: var(--spacing-lg); padding: var(--spacing-lg);
flex: 1;
display: flex;
flex-direction: column;
} }
.product-category { .product-category {
@@ -883,6 +886,7 @@ body {
justify-content: space-between; justify-content: space-between;
padding: var(--spacing-md) var(--spacing-lg); padding: var(--spacing-md) var(--spacing-lg);
border-top: 1px solid var(--border-light); border-top: 1px solid var(--border-light);
margin-top: auto;
} }
.product-stock { .product-stock {

View File

@@ -405,14 +405,14 @@
background: #f3f0ff; background: #f3f0ff;
} }
/* Responsive Design */ /* Responsive Design - Mobile & Tablet (up to 1024px includes all iPads) */
@media (max-width: 1024px) { @media (max-width: 1024px) {
.navbar-menu { .navbar-menu {
display: none; display: none !important;
} }
.mobile-toggle { .mobile-toggle {
display: flex; display: flex !important;
} }
.navbar-brand { .navbar-brand {
@@ -423,6 +423,17 @@
.navbar-actions { .navbar-actions {
margin-left: 16px; margin-left: 16px;
} }
/* Wider mobile menu for tablets */
.mobile-menu {
width: 350px;
max-width: 85%;
}
.mobile-link {
padding: 16px 20px;
font-size: 17px;
}
} }
@media (max-width: 640px) { @media (max-width: 640px) {
@@ -456,13 +467,88 @@
right: -16px; right: -16px;
} }
} }
/* Tablet-specific (iPad) enhancements - portrait and landscape */
@media (min-width: 641px) and (max-width: 1024px) {
.navbar-wrapper {
padding: 0 24px;
height: 72px;
}
.brand-logo {
width: 52px;
height: 52px;
}
.brand-name {
font-size: 20px;
}
/* Larger hamburger menu for tablets */
.mobile-toggle {
width: 48px;
height: 48px;
padding: 12px;
}
.toggle-line {
width: 26px;
height: 3px;
}
/* Wider mobile menu for tablets */
.mobile-menu {
width: 380px;
max-width: 75%;
padding: 24px;
}
.mobile-menu-header {
padding-bottom: 20px;
margin-bottom: 28px;
}
.mobile-brand {
font-size: 20px;
}
.mobile-close {
font-size: 28px;
width: 44px;
height: 44px;
}
.mobile-link {
padding: 18px 24px;
font-size: 18px;
border-radius: 10px;
}
.mobile-menu-list li {
margin-bottom: 6px;
}
/* Larger action buttons for tablets */
.action-btn {
width: 48px;
height: 48px;
font-size: 24px;
}
.action-badge {
min-width: 20px;
height: 20px;
font-size: 12px;
}
}
/** /**
* Mobile Navbar Fixes * Mobile Navbar Fixes
* Ensures hamburger menu, cart, and wishlist are visible on mobile devices * Ensures hamburger menu, cart, and wishlist are visible on mobile/tablet devices
*/ */
/* Mobile hamburger menu - always visible on small screens */ /* Mobile/Tablet hamburger menu - visible on screens up to 1024px (includes iPads) */
@media (max-width: 768px) { @media (max-width: 1024px) {
.mobile-toggle { .mobile-toggle {
display: flex !important; display: flex !important;
flex-direction: column; flex-direction: column;
@@ -676,8 +762,17 @@
} }
} }
/* Tablet adjustments */ /* Desktop - hide mobile elements (only on screens larger than 1024px) */
@media (min-width: 769px) and (max-width: 1024px) { @media (min-width: 1025px) {
.mobile-toggle {
display: none !important;
}
.mobile-menu,
.mobile-menu-overlay {
display: none !important;
}
.navbar-actions { .navbar-actions {
gap: 16px; gap: 16px;
} }
@@ -691,18 +786,6 @@
} }
} }
/* Desktop - hide mobile elements */
@media (min-width: 769px) {
.mobile-toggle {
display: none !important;
}
.mobile-menu,
.mobile-menu-overlay {
display: none !important;
}
}
/* Accessibility improvements */ /* Accessibility improvements */
.action-btn:focus, .action-btn:focus,
.mobile-toggle:focus, .mobile-toggle:focus,

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@@ -41,19 +41,67 @@ const SkyArtShop = {
}); });
} }
// Mobile menu toggle // Mobile/Tablet menu toggle
if (mobileToggle && navMenu) { if (mobileToggle && navMenu) {
// Create overlay element for background dimming
let overlay = document.querySelector(".nav-menu-overlay");
if (!overlay) {
overlay = document.createElement("div");
overlay.className = "nav-menu-overlay";
document.body.appendChild(overlay);
}
// Create close button inside menu
let closeBtn = navMenu.querySelector(".nav-menu-close");
if (!closeBtn) {
closeBtn = document.createElement("button");
closeBtn.className = "nav-menu-close";
closeBtn.innerHTML = '<i class="bi bi-x-lg"></i>';
closeBtn.setAttribute("aria-label", "Close menu");
navMenu.insertBefore(closeBtn, navMenu.firstChild);
}
// Function to open menu
const openMenu = () => {
navMenu.classList.add("open");
mobileToggle.classList.add("active");
overlay.classList.add("active");
document.body.classList.add("nav-menu-open");
};
// Function to close menu
const closeMenu = () => {
navMenu.classList.remove("open");
mobileToggle.classList.remove("active");
overlay.classList.remove("active");
document.body.classList.remove("nav-menu-open");
};
// Toggle button click
mobileToggle.addEventListener("click", (e) => { mobileToggle.addEventListener("click", (e) => {
e.stopPropagation(); e.stopPropagation();
navMenu.classList.toggle("open"); if (navMenu.classList.contains("open")) {
mobileToggle.classList.toggle("active"); closeMenu();
} else {
openMenu();
}
});
// Close button click
closeBtn.addEventListener("click", (e) => {
e.stopPropagation();
closeMenu();
});
// Overlay click to close
overlay.addEventListener("click", () => {
closeMenu();
}); });
// Close menu when clicking a link // Close menu when clicking a link
navMenu.querySelectorAll(".nav-link").forEach((link) => { navMenu.querySelectorAll(".nav-link").forEach((link) => {
link.addEventListener("click", () => { link.addEventListener("click", () => {
navMenu.classList.remove("open"); closeMenu();
mobileToggle.classList.remove("active");
}); });
}); });
@@ -64,12 +112,11 @@ const SkyArtShop = {
!navMenu.contains(e.target) && !navMenu.contains(e.target) &&
!mobileToggle.contains(e.target) !mobileToggle.contains(e.target)
) { ) {
navMenu.classList.remove("open"); closeMenu();
mobileToggle.classList.remove("active");
} }
}); });
// Close menu when touching outside (for mobile) // Close menu when touching outside (for mobile/tablet)
document.addEventListener( document.addEventListener(
"touchstart", "touchstart",
(e) => { (e) => {
@@ -78,12 +125,18 @@ const SkyArtShop = {
!navMenu.contains(e.target) && !navMenu.contains(e.target) &&
!mobileToggle.contains(e.target) !mobileToggle.contains(e.target)
) { ) {
navMenu.classList.remove("open"); closeMenu();
mobileToggle.classList.remove("active");
} }
}, },
{ passive: true }, { passive: true },
); );
// Close menu on escape key
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && navMenu.classList.contains("open")) {
closeMenu();
}
});
} }
// Set active nav link // Set active nav link

View File

@@ -25,13 +25,10 @@
/> />
<!-- Modern Theme CSS --> <!-- Modern Theme CSS -->
<link rel="stylesheet" href="/assets/css/modern-theme.css?v=fix35" />
<link <link
rel="stylesheet" rel="stylesheet"
href="/assets/css/modern-theme.css?v=fix33" href="/assets/css/mobile-fixes.css?v=20260120fix2"
/>
<link
rel="stylesheet"
href="/assets/css/mobile-fixes.css?v=20260118fix10"
/> />
<style> <style>
@@ -885,11 +882,14 @@
</p> </p>
<form <form
class="newsletter-form" class="newsletter-form"
id="blogNewsletterForm"
style="max-width: 500px; margin: 0 auto; display: flex; gap: 12px" style="max-width: 500px; margin: 0 auto; display: flex; gap: 12px"
> >
<input <input
type="email" type="email"
id="blogNewsletterEmail"
placeholder="Enter your email" placeholder="Enter your email"
required
style=" style="
flex: 1; flex: 1;
padding: 16px 24px; padding: 16px 24px;
@@ -898,7 +898,13 @@
font-size: 1rem; font-size: 1rem;
" "
/> />
<button type="submit" class="btn btn-primary">Subscribe</button> <button
type="submit"
class="btn btn-primary"
id="blogNewsletterBtn"
>
Subscribe
</button>
</form> </form>
</div> </div>
</section> </section>
@@ -922,14 +928,62 @@
needs. needs.
</p> </p>
<div class="footer-social" id="footerSocialLinks"> <div class="footer-social" id="footerSocialLinks">
<a href="#" class="social-link" id="footerFacebook" style="display:none;"><i class="bi bi-facebook"></i></a> <a
<a href="#" class="social-link" id="footerInstagram" style="display:none;"><i class="bi bi-instagram"></i></a> href="#"
<a href="#" class="social-link" id="footerTwitter" style="display:none;"><i class="bi bi-twitter-x"></i></a> class="social-link"
<a href="#" class="social-link" id="footerYoutube" style="display:none;"><i class="bi bi-youtube"></i></a> id="footerFacebook"
<a href="#" class="social-link" id="footerPinterest" style="display:none;"><i class="bi bi-pinterest"></i></a> style="display: none"
<a href="#" class="social-link" id="footerTiktok" style="display:none;"><i class="bi bi-tiktok"></i></a> ><i class="bi bi-facebook"></i
<a href="#" class="social-link" id="footerWhatsapp" style="display:none;"><i class="bi bi-whatsapp"></i></a> ></a>
<a href="#" class="social-link" id="footerLinkedin" style="display:none;"><i class="bi bi-linkedin"></i></a> <a
href="#"
class="social-link"
id="footerInstagram"
style="display: none"
><i class="bi bi-instagram"></i
></a>
<a
href="#"
class="social-link"
id="footerTwitter"
style="display: none"
><i class="bi bi-twitter-x"></i
></a>
<a
href="#"
class="social-link"
id="footerYoutube"
style="display: none"
><i class="bi bi-youtube"></i
></a>
<a
href="#"
class="social-link"
id="footerPinterest"
style="display: none"
><i class="bi bi-pinterest"></i
></a>
<a
href="#"
class="social-link"
id="footerTiktok"
style="display: none"
><i class="bi bi-tiktok"></i
></a>
<a
href="#"
class="social-link"
id="footerWhatsapp"
style="display: none"
><i class="bi bi-whatsapp"></i
></a>
<a
href="#"
class="social-link"
id="footerLinkedin"
style="display: none"
><i class="bi bi-linkedin"></i
></a>
</div> </div>
</div> </div>
@@ -966,7 +1020,7 @@
</div> </div>
<div class="footer-bottom"> <div class="footer-bottom">
<p>&copy; 2026 Sky Art Shop. All rights reserved.</p> <p>&copy; 2026 PromptTech-Solution. Designed and Developed by: PromptTech-Solution</p>
<p> <p>
Made with Made with
<i class="bi bi-heart-fill" style="color: var(--primary-pink)"></i> <i class="bi bi-heart-fill" style="color: var(--primary-pink)"></i>
@@ -1373,6 +1427,61 @@
document.addEventListener("keydown", (e) => { document.addEventListener("keydown", (e) => {
if (e.key === "Escape") closeBlogDetail(); if (e.key === "Escape") closeBlogDetail();
}); });
// Newsletter form handler
document
.getElementById("blogNewsletterForm")
?.addEventListener("submit", async function (e) {
e.preventDefault();
const emailInput = document.getElementById("blogNewsletterEmail");
const submitBtn = document.getElementById("blogNewsletterBtn");
const email = emailInput.value.trim();
if (!email) {
SkyArtShop.showNotification(
"Please enter your email address.",
"error",
);
return;
}
const originalBtnText = submitBtn.innerHTML;
submitBtn.disabled = true;
submitBtn.innerHTML = "Subscribing...";
try {
const response = await fetch("/api/newsletter/subscribe", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, source: "blog" }),
});
const data = await response.json();
if (data.success) {
SkyArtShop.showNotification(
data.message || "Successfully subscribed!",
"success",
);
emailInput.value = "";
} else {
SkyArtShop.showNotification(
data.message || "Failed to subscribe. Please try again.",
"error",
);
}
} catch (error) {
console.error("Newsletter error:", error);
SkyArtShop.showNotification(
"Failed to subscribe. Please try again later.",
"error",
);
} finally {
submitBtn.disabled = false;
submitBtn.innerHTML = originalBtnText;
}
});
}); });
</script> </script>
<script src="/assets/js/accessibility.js"></script> <script src="/assets/js/accessibility.js"></script>

View File

@@ -22,8 +22,8 @@
/> />
<!-- Modern Theme CSS --> <!-- Modern Theme CSS -->
<link rel="stylesheet" href="/assets/css/modern-theme.css?v=fix33" /> <link rel="stylesheet" href="/assets/css/modern-theme.css?v=fix35" />
<link rel="stylesheet" href="/assets/css/mobile-fixes.css?v=20260118c" /> <link rel="stylesheet" href="/assets/css/mobile-fixes.css?v=20260120fix2" />
<style> <style>
.checkout-container { .checkout-container {
@@ -640,7 +640,7 @@
</div> </div>
<div class="footer-bottom"> <div class="footer-bottom">
<p>&copy; 2026 Sky Art Shop. All rights reserved.</p> <p>&copy; 2026 PromptTech-Solution. Designed and Developed by: PromptTech-Solution</p>
<p> <p>
Made with Made with
<i class="bi bi-heart-fill" style="color: var(--primary-pink)"></i> <i class="bi bi-heart-fill" style="color: var(--primary-pink)"></i>

View File

@@ -25,10 +25,10 @@
/> />
<!-- Modern Theme CSS --> <!-- Modern Theme CSS -->
<link rel="stylesheet" href="/assets/css/modern-theme.css?v=fix33" /> <link rel="stylesheet" href="/assets/css/modern-theme.css?v=fix35" />
<link <link
rel="stylesheet" rel="stylesheet"
href="/assets/css/mobile-fixes.css?v=20260118fix10" href="/assets/css/mobile-fixes.css?v=20260120fix2"
/> />
<style> <style>
@@ -671,7 +671,7 @@
</div> </div>
<div class="footer-bottom"> <div class="footer-bottom">
<p>&copy; 2026 Sky Art Shop. All rights reserved.</p> <p>&copy; 2026 PromptTech-Solution. Designed and Developed by: PromptTech-Solution</p>
<p> <p>
Made with Made with
<i class="bi bi-heart-fill" style="color: var(--primary-pink)"></i> <i class="bi bi-heart-fill" style="color: var(--primary-pink)"></i>
@@ -835,16 +835,62 @@
document document
.getElementById("contactForm") .getElementById("contactForm")
.addEventListener("submit", function (e) { .addEventListener("submit", async function (e) {
e.preventDefault(); e.preventDefault();
// Show success notification const form = this;
SkyArtShop.showNotification( const submitBtn = form.querySelector('button[type="submit"]');
"Message sent successfully! We'll get back to you soon.", const originalBtnText = submitBtn.innerHTML;
);
// Reset form // Get form data
this.reset(); const name = document.getElementById("name").value.trim();
const email = document.getElementById("email").value.trim();
const subject = document.getElementById("subject").value.trim();
const message = document.getElementById("message").value.trim();
// Validation
if (!name || !email || !subject || !message) {
SkyArtShop.showNotification("Please fill in all fields.", "error");
return;
}
// Disable button and show loading
submitBtn.disabled = true;
submitBtn.innerHTML =
'<i class="bi bi-hourglass-split"></i> Sending...';
try {
const response = await fetch("/api/contact", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name, email, subject, message }),
});
const data = await response.json();
if (data.success) {
SkyArtShop.showNotification(
data.message ||
"Message sent successfully! We'll get back to you soon.",
"success",
);
form.reset();
} else {
SkyArtShop.showNotification(
data.message || "Failed to send message. Please try again.",
"error",
);
}
} catch (error) {
console.error("Contact form error:", error);
SkyArtShop.showNotification(
"Failed to send message. Please try again later.",
"error",
);
} finally {
submitBtn.disabled = false;
submitBtn.innerHTML = originalBtnText;
}
}); });
</script> </script>
<script src="/assets/js/accessibility.js"></script> <script src="/assets/js/accessibility.js"></script>

View File

@@ -25,8 +25,8 @@
/> />
<!-- Modern Theme CSS --> <!-- Modern Theme CSS -->
<link rel="stylesheet" href="/assets/css/modern-theme.css?v=fix33" /> <link rel="stylesheet" href="/assets/css/modern-theme.css?v=fix35" />
<link rel="stylesheet" href="/assets/css/mobile-fixes.css?v=20260118c" /> <link rel="stylesheet" href="/assets/css/mobile-fixes.css?v=20260120fix2" />
<style> <style>
.faq-container { .faq-container {
@@ -252,7 +252,7 @@
</div> </div>
<div class="footer-bottom"> <div class="footer-bottom">
<p>&copy; 2026 Sky Art Shop. All rights reserved.</p> <p>&copy; 2026 PromptTech-Solution. Designed and Developed by: PromptTech-Solution</p>
<p> <p>
Made with Made with
<i class="bi bi-heart-fill" style="color: var(--primary-pink)"></i> <i class="bi bi-heart-fill" style="color: var(--primary-pink)"></i>

View File

@@ -25,20 +25,59 @@
/> />
<!-- Modern Theme CSS --> <!-- Modern Theme CSS -->
<link rel="stylesheet" href="/assets/css/modern-theme.css?v=fix33" /> <link rel="stylesheet" href="/assets/css/modern-theme.css?v=fix35" />
<link <link rel="stylesheet" href="/assets/css/mobile-fixes.css?v=20260120fix2" />
rel="stylesheet"
href="/assets/css/mobile-fixes.css?v=20260119touch"
/>
<style> <style>
/* Blog Grid for Get Inspired Section - Match Blog Page */ /* Get Inspired Section - Black subtitle text */
#get-inspired .section-subtitle {
color: var(--text-primary) !important;
}
/* Stay Connected Section - Black subtitle text */
#newsletter-section .section-subtitle {
color: var(--text-primary) !important;
}
/* Blog Grid for Get Inspired Section - Horizontal Scroll */
#inspirationGrid.blog-grid { #inspirationGrid.blog-grid {
display: grid; display: flex !important;
grid-template-columns: repeat(3, 1fr); flex-wrap: nowrap !important;
gap: var(--spacing-lg); overflow-x: auto !important;
max-width: 1100px; overflow-y: hidden !important;
margin: 0 auto; scroll-behavior: smooth;
-webkit-overflow-scrolling: touch;
scroll-snap-type: x mandatory;
gap: 20px;
padding: 8px 0 16px 0;
max-width: none;
margin: 0;
}
#inspirationGrid.blog-grid::-webkit-scrollbar {
height: 8px;
}
#inspirationGrid.blog-grid::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.5);
border-radius: 10px;
}
#inspirationGrid.blog-grid::-webkit-scrollbar-thumb {
background: var(--primary-pink-dark, #fcb1d8);
border-radius: 10px;
}
#inspirationGrid.blog-grid::-webkit-scrollbar-thumb:hover {
background: var(--primary-pink, #ffd0d0);
}
/* Blog cards in horizontal scroll */
#inspirationGrid.blog-grid .blog-card {
flex: 0 0 320px;
min-width: 320px;
max-width: 320px;
scroll-snap-align: start;
} }
/* Blog Card Styles for Get Inspired Section */ /* Blog Card Styles for Get Inspired Section */
@@ -157,15 +196,21 @@
flex: 1; flex: 1;
} }
/* Tablet - Blog cards horizontal scroll */
@media (max-width: 992px) { @media (max-width: 992px) {
#inspirationGrid.blog-grid { #inspirationGrid.blog-grid .blog-card {
grid-template-columns: repeat(2, 1fr); flex: 0 0 280px;
min-width: 280px;
max-width: 280px;
} }
} }
/* Mobile - Blog cards horizontal scroll */
@media (max-width: 576px) { @media (max-width: 576px) {
#inspirationGrid.blog-grid { #inspirationGrid.blog-grid .blog-card {
grid-template-columns: 1fr; flex: 0 0 240px;
min-width: 240px;
max-width: 240px;
} }
} }
@@ -307,7 +352,7 @@
<section <section
class="section" class="section"
id="featured-products" id="featured-products"
style="background: var(--primary-pink-light)" style="background: var(--primary-pink-light); padding: 30px 0"
> >
<div class="container"> <div class="container">
<div class="section-header"> <div class="section-header">
@@ -336,7 +381,7 @@
<section <section
class="section inspiration-section" class="section inspiration-section"
id="get-inspired" id="get-inspired"
style="background: var(--accent-pink)" style="background: var(--accent-pink); padding: 30px 0"
> >
<div class="container"> <div class="container">
<div class="section-header"> <div class="section-header">
@@ -362,10 +407,10 @@
<section <section
class="section" class="section"
id="about-preview" id="about-preview"
style="background: var(--primary-pink-dark); padding: 0; margin: 0" style="background: var(--primary-pink-light); padding: 0; margin: 0"
> >
<div class="container"> <div class="container">
<div class="about-content"> <div class="about-content" style="padding: 10px 0">
<div class="about-text"> <div class="about-text">
<h2 id="aboutTitle">About Sky Art Shop</h2> <h2 id="aboutTitle">About Sky Art Shop</h2>
<div id="aboutDescription"> <div id="aboutDescription">
@@ -407,13 +452,8 @@
<!-- Newsletter --> <!-- Newsletter -->
<section <section
class="section" class="section"
style=" id="newsletter-section"
background: linear-gradient( style="background: var(--accent-pink); padding: 40px 0"
135deg,
var(--primary-pink-light) 0%,
var(--primary-pink) 100%
);
"
> >
<div class="container"> <div class="container">
<div class="text-center"> <div class="text-center">
@@ -424,11 +464,14 @@
</p> </p>
<form <form
class="newsletter-form" class="newsletter-form"
id="homeNewsletterForm"
style="max-width: 500px; margin: 0 auto; display: flex; gap: 12px" style="max-width: 500px; margin: 0 auto; display: flex; gap: 12px"
> >
<input <input
type="email" type="email"
id="homeNewsletterEmail"
placeholder="Enter your email" placeholder="Enter your email"
required
style=" style="
flex: 1; flex: 1;
padding: 16px 24px; padding: 16px 24px;
@@ -437,7 +480,13 @@
font-size: 1rem; font-size: 1rem;
" "
/> />
<button type="submit" class="btn btn-primary">Subscribe</button> <button
type="submit"
class="btn btn-primary"
id="homeNewsletterBtn"
>
Subscribe
</button>
</form> </form>
</div> </div>
</div> </div>
@@ -519,6 +568,10 @@
><i class="bi bi-linkedin"></i ><i class="bi bi-linkedin"></i
></a> ></a>
</div> </div>
<p style="margin-top: 12px; font-size: 0.85rem; opacity: 0.9">
&copy; 2026 PromptTech-Solution.<br />Designed and Developed by:
PromptTech-Solution
</p>
</div> </div>
<div class="footer-column"> <div class="footer-column">
@@ -554,7 +607,7 @@
</div> </div>
<div class="footer-bottom"> <div class="footer-bottom">
<p>&copy; 2026 Sky Art Shop. All rights reserved.</p> <p>&copy; 2026 PromptTech-Solution. Designed and Developed by: PromptTech-Solution</p>
<p> <p>
Made with Made with
<i class="bi bi-heart-fill" style="color: var(--primary-pink)"></i> <i class="bi bi-heart-fill" style="color: var(--primary-pink)"></i>
@@ -1010,6 +1063,61 @@
heroSlider.innerHTML = heroSlider.innerHTML =
slidesHtml + `<div class="slider-nav">${dotsHtml}</div>` + arrowsHtml; slidesHtml + `<div class="slider-nav">${dotsHtml}</div>` + arrowsHtml;
} }
// Newsletter form handler
document
.getElementById("homeNewsletterForm")
?.addEventListener("submit", async function (e) {
e.preventDefault();
const emailInput = document.getElementById("homeNewsletterEmail");
const submitBtn = document.getElementById("homeNewsletterBtn");
const email = emailInput.value.trim();
if (!email) {
SkyArtShop.showNotification(
"Please enter your email address.",
"error",
);
return;
}
const originalBtnText = submitBtn.innerHTML;
submitBtn.disabled = true;
submitBtn.innerHTML = "Subscribing...";
try {
const response = await fetch("/api/newsletter/subscribe", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, source: "home" }),
});
const data = await response.json();
if (data.success) {
SkyArtShop.showNotification(
data.message || "Successfully subscribed!",
"success",
);
emailInput.value = "";
} else {
SkyArtShop.showNotification(
data.message || "Failed to subscribe. Please try again.",
"error",
);
}
} catch (error) {
console.error("Newsletter error:", error);
SkyArtShop.showNotification(
"Failed to subscribe. Please try again later.",
"error",
);
} finally {
submitBtn.disabled = false;
submitBtn.innerHTML = originalBtnText;
}
});
</script> </script>
<script src="/assets/js/accessibility.js"></script> <script src="/assets/js/accessibility.js"></script>
</body> </body>

View File

@@ -27,11 +27,11 @@
<!-- Modern Theme CSS --> <!-- Modern Theme CSS -->
<link <link
rel="stylesheet" rel="stylesheet"
href="/assets/css/modern-theme.css?v=fix33" href="/assets/css/modern-theme.css?v=fix35"
/> />
<link <link
rel="stylesheet" rel="stylesheet"
href="/assets/css/mobile-fixes.css?v=20260118fix10" href="/assets/css/mobile-fixes.css?v=20260120fix2"
/> />
<style> <style>
@@ -567,7 +567,7 @@
</div> </div>
<div class="footer-bottom"> <div class="footer-bottom">
<p>&copy; 2026 Sky Art Shop. All rights reserved.</p> <p>&copy; 2026 PromptTech-Solution. Designed and Developed by: PromptTech-Solution</p>
<p> <p>
Made with Made with
<i class="bi bi-heart-fill" style="color: var(--primary-pink)"></i> <i class="bi bi-heart-fill" style="color: var(--primary-pink)"></i>

View File

@@ -22,8 +22,8 @@
/> />
<!-- Modern Theme CSS --> <!-- Modern Theme CSS -->
<link rel="stylesheet" href="/assets/css/modern-theme.css?v=fix33" /> <link rel="stylesheet" href="/assets/css/modern-theme.css?v=fix35" />
<link rel="stylesheet" href="/assets/css/mobile-fixes.css?v=fix32" /> <link rel="stylesheet" href="/assets/css/mobile-fixes.css?v=20260120fix2" />
<style> <style>
.policy-container { .policy-container {
@@ -278,7 +278,7 @@
</div> </div>
<div class="footer-bottom"> <div class="footer-bottom">
<p>&copy; 2026 Sky Art Shop. All rights reserved.</p> <p>&copy; 2026 PromptTech-Solution. Designed and Developed by: PromptTech-Solution</p>
<p> <p>
Made with Made with
<i class="bi bi-heart-fill" style="color: var(--primary-pink)"></i> <i class="bi bi-heart-fill" style="color: var(--primary-pink)"></i>

View File

@@ -22,8 +22,8 @@
/> />
<!-- Modern Theme CSS --> <!-- Modern Theme CSS -->
<link rel="stylesheet" href="/assets/css/modern-theme.css?v=fix34" /> <link rel="stylesheet" href="/assets/css/modern-theme.css?v=fix35" />
<link rel="stylesheet" href="/assets/css/mobile-fixes.css?v=20260119fix3" /> <link rel="stylesheet" href="/assets/css/mobile-fixes.css?v=20260120fix2" />
<style> <style>
/* Product page breadcrumb - force single line */ /* Product page breadcrumb - force single line */
@@ -121,6 +121,64 @@
} }
} }
/* Tablet: iPad, iPad Air, iPad Pro (768px - 1024px) */
@media (min-width: 769px) and (max-width: 1024px) {
.product-detail {
display: grid !important;
grid-template-columns: 1fr 1fr !important;
gap: var(--spacing-xl) !important;
width: 100% !important;
max-width: 100% !important;
box-sizing: border-box !important;
}
.product-gallery {
position: relative !important;
width: 100% !important;
max-width: 100% !important;
overflow: hidden !important;
}
.main-image {
position: relative !important;
width: 100% !important;
max-width: 100% !important;
height: 0 !important;
padding-bottom: 100% !important;
overflow: hidden !important;
border-radius: var(--radius-lg) !important;
background: #ffffff !important;
}
.main-image img {
position: absolute !important;
top: 0 !important;
left: 0 !important;
width: 100% !important;
height: 100% !important;
object-fit: cover !important;
object-position: center !important;
}
.product-details {
width: 100% !important;
max-width: 100% !important;
box-sizing: border-box !important;
}
.product-details h1,
#productName {
font-size: 1.5rem !important;
word-break: break-word !important;
overflow-wrap: break-word !important;
}
.container {
max-width: 100% !important;
overflow-x: hidden !important;
}
}
.product-detail { .product-detail {
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
@@ -134,8 +192,11 @@
} }
.main-image { .main-image {
position: relative;
width: 100%; width: 100%;
aspect-ratio: 1; max-width: 100%;
height: 0;
padding-bottom: 100%; /* 1:1 aspect ratio */
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
overflow: hidden; overflow: hidden;
background: var(--bg-white); background: var(--bg-white);
@@ -143,12 +204,79 @@
} }
.main-image img { .main-image img {
position: absolute;
top: 0;
left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: cover; object-fit: cover;
object-position: center;
transition: var(--transition-smooth); transition: var(--transition-smooth);
} }
/* Gallery Navigation Arrows */
.gallery-arrow {
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 44px;
height: 44px;
background: rgba(255, 255, 255, 0.9);
border: none;
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.2rem;
color: var(--text-primary);
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.15);
transition: all 0.2s ease;
z-index: 10;
opacity: 0;
}
.main-image:hover .gallery-arrow {
opacity: 1;
}
.gallery-arrow:hover {
background: white;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
transform: translateY(-50%) scale(1.1);
}
.gallery-arrow.prev {
left: 12px;
}
.gallery-arrow.next {
right: 12px;
}
.gallery-arrow:disabled {
opacity: 0.3;
cursor: not-allowed;
}
/* Mobile: always show arrows */
@media (max-width: 768px) {
.gallery-arrow {
opacity: 1;
width: 36px;
height: 36px;
font-size: 1rem;
}
.gallery-arrow.prev {
left: 8px;
}
.gallery-arrow.next {
right: 8px;
}
}
.thumbnail-gallery { .thumbnail-gallery {
display: flex; display: flex;
gap: var(--spacing-sm); gap: var(--spacing-sm);
@@ -679,11 +807,25 @@
<!-- Gallery --> <!-- Gallery -->
<div class="product-gallery"> <div class="product-gallery">
<div class="main-image"> <div class="main-image">
<button
class="gallery-arrow prev"
id="galleryPrev"
aria-label="Previous image"
>
<i class="bi bi-chevron-left"></i>
</button>
<img <img
src="https://images.unsplash.com/photo-1513519245088-0e12902e35a6?w=800&q=80" src="https://images.unsplash.com/photo-1513519245088-0e12902e35a6?w=800&q=80"
alt="Product" alt="Product"
id="mainImage" id="mainImage"
/> />
<button
class="gallery-arrow next"
id="galleryNext"
aria-label="Next image"
>
<i class="bi bi-chevron-right"></i>
</button>
</div> </div>
<div class="thumbnail-gallery" id="thumbnailGallery"> <div class="thumbnail-gallery" id="thumbnailGallery">
<!-- Thumbnails loaded via JS --> <!-- Thumbnails loaded via JS -->
@@ -939,7 +1081,10 @@
</div> </div>
<div class="footer-bottom"> <div class="footer-bottom">
<p>&copy; 2026 Sky Art Shop. All rights reserved.</p> <p>
&copy; 2026 PromptTech-Solution. Designed and Developed by:
PromptTech-Solution
</p>
<p> <p>
Made with Made with
<i class="bi bi-heart-fill" style="color: var(--primary-pink)"></i> <i class="bi bi-heart-fill" style="color: var(--primary-pink)"></i>
@@ -1260,8 +1405,76 @@
.forEach((t) => t.classList.remove("active")); .forEach((t) => t.classList.remove("active"));
thumb.classList.add("active"); thumb.classList.add("active");
mainImage.src = thumb.dataset.image; mainImage.src = thumb.dataset.image;
updateArrowState();
}); });
}); });
// Gallery arrow navigation
let currentImageIndex = 0;
const galleryPrev = document.getElementById("galleryPrev");
const galleryNext = document.getElementById("galleryNext");
function updateArrowState() {
const thumbnails = thumbnailGallery.querySelectorAll(".thumbnail");
thumbnails.forEach((thumb, index) => {
if (thumb.classList.contains("active")) {
currentImageIndex = index;
}
});
// Update button states
if (galleryPrev) galleryPrev.disabled = currentImageIndex === 0;
if (galleryNext)
galleryNext.disabled =
currentImageIndex === thumbnails.length - 1;
// Hide arrows if only one image
if (thumbnails.length <= 1) {
if (galleryPrev) galleryPrev.style.display = "none";
if (galleryNext) galleryNext.style.display = "none";
}
}
function navigateGallery(direction) {
const thumbnails = thumbnailGallery.querySelectorAll(".thumbnail");
if (thumbnails.length === 0) return;
currentImageIndex += direction;
if (currentImageIndex < 0) currentImageIndex = 0;
if (currentImageIndex >= thumbnails.length)
currentImageIndex = thumbnails.length - 1;
const targetThumb = thumbnails[currentImageIndex];
thumbnails.forEach((t) => t.classList.remove("active"));
targetThumb.classList.add("active");
mainImage.src = targetThumb.dataset.image;
// Scroll thumbnail into view
targetThumb.scrollIntoView({
behavior: "smooth",
block: "nearest",
inline: "center",
});
updateArrowState();
}
if (galleryPrev) {
galleryPrev.addEventListener("click", (e) => {
e.preventDefault();
navigateGallery(-1);
});
}
if (galleryNext) {
galleryNext.addEventListener("click", (e) => {
e.preventDefault();
navigateGallery(1);
});
}
// Initialize arrow state
updateArrowState();
} }
// Color options // Color options

View File

@@ -25,8 +25,8 @@
/> />
<!-- Modern Theme CSS --> <!-- Modern Theme CSS -->
<link rel="stylesheet" href="/assets/css/modern-theme.css?v=fix33" /> <link rel="stylesheet" href="/assets/css/modern-theme.css?v=fix35" />
<link rel="stylesheet" href="/assets/css/mobile-fixes.css?v=fix32" /> <link rel="stylesheet" href="/assets/css/mobile-fixes.css?v=20260120fix2" />
<style> <style>
.policy-container { .policy-container {
@@ -292,7 +292,7 @@
</div> </div>
<div class="footer-bottom"> <div class="footer-bottom">
<p>&copy; 2026 Sky Art Shop. All rights reserved.</p> <p>&copy; 2026 PromptTech-Solution. Designed and Developed by: PromptTech-Solution</p>
<p> <p>
Made with Made with
<i class="bi bi-heart-fill" style="color: var(--primary-pink)"></i> <i class="bi bi-heart-fill" style="color: var(--primary-pink)"></i>

View File

@@ -22,8 +22,8 @@
/> />
<!-- Modern Theme CSS --> <!-- Modern Theme CSS -->
<link rel="stylesheet" href="/assets/css/modern-theme.css?v=fix33" /> <link rel="stylesheet" href="/assets/css/modern-theme.css?v=fix35" />
<link rel="stylesheet" href="/assets/css/mobile-fixes.css?v=fix32" /> <link rel="stylesheet" href="/assets/css/mobile-fixes.css?v=20260120fix2" />
<style> <style>
.policy-container { .policy-container {
@@ -331,7 +331,7 @@
</div> </div>
<div class="footer-bottom"> <div class="footer-bottom">
<p>&copy; 2026 Sky Art Shop. All rights reserved.</p> <p>&copy; 2026 PromptTech-Solution. Designed and Developed by: PromptTech-Solution</p>
<p> <p>
Made with Made with
<i class="bi bi-heart-fill" style="color: var(--primary-pink)"></i> <i class="bi bi-heart-fill" style="color: var(--primary-pink)"></i>

View File

@@ -22,8 +22,8 @@
/> />
<!-- Modern Theme CSS --> <!-- Modern Theme CSS -->
<link rel="stylesheet" href="/assets/css/modern-theme.css?v=fix34" /> <link rel="stylesheet" href="/assets/css/modern-theme.css?v=fix35" />
<link rel="stylesheet" href="/assets/css/mobile-fixes.css?v=20260119fix1" /> <link rel="stylesheet" href="/assets/css/mobile-fixes.css?v=20260120fix2" />
<style> <style>
/* Mobile & Tablet: Shop page layout fixes */ /* Mobile & Tablet: Shop page layout fixes */
@media (max-width: 992px) { @media (max-width: 992px) {
@@ -541,7 +541,10 @@
</div> </div>
<div class="footer-bottom"> <div class="footer-bottom">
<p>&copy; 2026 Sky Art Shop. All rights reserved.</p> <p>
&copy; 2026 PromptTech-Solution. Designed and Developed by:
PromptTech-Solution
</p>
<p> <p>
Made with Made with
<i class="bi bi-heart-fill" style="color: var(--primary-pink)"></i> <i class="bi bi-heart-fill" style="color: var(--primary-pink)"></i>