webupdate
This commit is contained in:
@@ -28,6 +28,6 @@ MAX_FILE_SIZE=62914560
|
||||
SMTP_HOST=smtp.gmail.com
|
||||
SMTP_PORT=587
|
||||
SMTP_SECURE=false
|
||||
SMTP_USER=YOUR_GMAIL@gmail.com
|
||||
SMTP_USER=skyartshop12.11@gmail.com
|
||||
SMTP_PASS=YOUR_APP_PASSWORD
|
||||
SMTP_FROM="Sky Art Shop" <YOUR_GMAIL@gmail.com>
|
||||
SMTP_FROM="Sky Art Shop" <skyartshop12.11@gmail.com>
|
||||
|
||||
36
backend/migrations/009_create_contact_newsletter.sql
Normal file
36
backend/migrations/009_create_contact_newsletter.sql
Normal 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';
|
||||
394
backend/routes/contact-newsletter.js
Normal file
394
backend/routes/contact-newsletter.js
Normal 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;
|
||||
@@ -257,6 +257,7 @@ const usersRoutes = require("./routes/users");
|
||||
const uploadRoutes = require("./routes/upload");
|
||||
const customerAuthRoutes = require("./routes/customer-auth");
|
||||
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)
|
||||
app.get("/admin", (req, res) => {
|
||||
@@ -330,6 +331,7 @@ app.use("/api/admin/users", usersRoutes);
|
||||
app.use("/api/admin", uploadRoutes);
|
||||
app.use("/api/customers", customerAuthRoutes);
|
||||
app.use("/api/customers", customerCartRoutes);
|
||||
app.use("/api", contactNewsletterRoutes);
|
||||
app.use("/api", publicRoutes);
|
||||
|
||||
// Admin static files (must be after URL rewriting)
|
||||
|
||||
@@ -69,6 +69,126 @@
|
||||
.stat-item i {
|
||||
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>
|
||||
</head>
|
||||
<body>
|
||||
@@ -162,6 +282,14 @@
|
||||
>
|
||||
<i class="bi bi-folder-plus"></i> New Folder
|
||||
</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="search-box">
|
||||
<i class="bi bi-search"></i>
|
||||
@@ -430,14 +558,105 @@
|
||||
let sortBy = localStorage.getItem("mediaLibrarySort") || "date";
|
||||
let searchQuery = "";
|
||||
let draggedItem = null;
|
||||
let isLoading = false;
|
||||
|
||||
// Initialize
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
initViewMode();
|
||||
showSkeletonLoading();
|
||||
loadContent();
|
||||
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() {
|
||||
const content = document.getElementById("mediaContent");
|
||||
content.classList.remove("grid-view", "list-view");
|
||||
@@ -538,11 +757,16 @@
|
||||
if (e.key === "Enter") confirmRename();
|
||||
});
|
||||
|
||||
// Delete selected
|
||||
// Delete selected (bottom button)
|
||||
document
|
||||
.getElementById("deleteSelectedBtn")
|
||||
.addEventListener("click", deleteSelected);
|
||||
|
||||
// Delete selected (top button)
|
||||
document
|
||||
.getElementById("deleteSelectedBtnTop")
|
||||
.addEventListener("click", deleteSelected);
|
||||
|
||||
// Back button
|
||||
document
|
||||
.getElementById("backBtn")
|
||||
@@ -569,7 +793,11 @@
|
||||
}
|
||||
|
||||
async function loadContent() {
|
||||
isLoading = true;
|
||||
displayedCount = 0;
|
||||
|
||||
try {
|
||||
// Load folders and files in parallel
|
||||
const [foldersRes, filesRes] = await Promise.all([
|
||||
fetch("/api/admin/folders", { credentials: "include" }),
|
||||
fetch(
|
||||
@@ -588,22 +816,34 @@
|
||||
folders = foldersData.folders || [];
|
||||
files = filesData.files || [];
|
||||
|
||||
updateStats();
|
||||
// Render content immediately (first batch)
|
||||
renderContent();
|
||||
updateBreadcrumb();
|
||||
|
||||
// Update stats in background (non-blocking)
|
||||
updateStatsAsync();
|
||||
} catch (error) {
|
||||
console.error("Error loading 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() {
|
||||
// Count all folders
|
||||
function updateStatsAsync() {
|
||||
// Update folder count immediately from current data
|
||||
document.getElementById("folderCount").textContent = `${
|
||||
folders.length
|
||||
} folder${folders.length !== 1 ? "s" : ""}`;
|
||||
|
||||
// Need to get total file count
|
||||
// Fetch total file stats in background
|
||||
fetch("/api/admin/uploads", { credentials: "include" })
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
@@ -618,32 +858,18 @@
|
||||
);
|
||||
document.getElementById("totalSize").textContent =
|
||||
formatFileSize(totalBytes) + " used";
|
||||
})
|
||||
.catch(() => {
|
||||
// Silently fail - stats are not critical
|
||||
});
|
||||
}
|
||||
|
||||
function renderContent() {
|
||||
const content = document.getElementById("mediaContent");
|
||||
|
||||
// Filter folders for current directory
|
||||
let filteredFolders = folders.filter((f) =>
|
||||
currentFolder ? f.parentId === currentFolder : f.parentId === null,
|
||||
);
|
||||
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);
|
||||
// Get filtered content
|
||||
const filteredFolders = getFilteredFolders();
|
||||
const filteredFiles = sortFiles(getFilteredFiles());
|
||||
|
||||
if (filteredFolders.length === 0 && filteredFiles.length === 0) {
|
||||
content.innerHTML = `
|
||||
@@ -671,9 +897,7 @@
|
||||
);
|
||||
html += `
|
||||
<div class="media-item folder-item ${isSelected ? "selected" : ""}"
|
||||
data-type="folder" data-id="${
|
||||
folder.id
|
||||
}" data-name="${escapeHtml(folder.name)}"
|
||||
data-type="folder" data-id="${folder.id}" data-name="${escapeHtml(folder.name)}"
|
||||
draggable="true">
|
||||
<div class="item-checkbox">
|
||||
<input type="checkbox" ${isSelected ? "checked" : ""}>
|
||||
@@ -697,7 +921,7 @@
|
||||
`;
|
||||
});
|
||||
|
||||
// Files
|
||||
// Files - use data-src for lazy loading
|
||||
filteredFiles.forEach((file) => {
|
||||
const isSelected = selectedItems.some(
|
||||
(s) => s.type === "file" && s.id === file.id,
|
||||
@@ -705,22 +929,17 @@
|
||||
html += `
|
||||
<div class="media-item file-item ${isSelected ? "selected" : ""}"
|
||||
data-type="file" data-id="${file.id}" data-path="${file.path}"
|
||||
data-name="${escapeHtml(file.originalName)}" data-size="${
|
||||
file.size
|
||||
}"
|
||||
data-name="${escapeHtml(file.originalName)}" data-size="${file.size}"
|
||||
draggable="true">
|
||||
<div class="item-checkbox">
|
||||
<input type="checkbox" ${isSelected ? "checked" : ""}>
|
||||
</div>
|
||||
<div class="item-preview">
|
||||
<img src="${file.path}" alt="${escapeHtml(
|
||||
file.originalName,
|
||||
)}" loading="lazy">
|
||||
<img data-src="${file.path}" alt="${escapeHtml(file.originalName)}"
|
||||
src="data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=">
|
||||
</div>
|
||||
<div class="item-info">
|
||||
<span class="item-name" title="${escapeHtml(
|
||||
file.originalName,
|
||||
)}">${escapeHtml(file.originalName)}</span>
|
||||
<span class="item-name" title="${escapeHtml(file.originalName)}">${escapeHtml(file.originalName)}</span>
|
||||
<span class="item-meta">${formatFileSize(file.size)}</span>
|
||||
</div>
|
||||
<div class="item-actions">
|
||||
@@ -740,6 +959,61 @@
|
||||
|
||||
content.innerHTML = html;
|
||||
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() {
|
||||
@@ -859,14 +1133,17 @@
|
||||
const count = selectedItems.length;
|
||||
const countEl = document.getElementById("selectedCount");
|
||||
const deleteBtn = document.getElementById("deleteSelectedBtn");
|
||||
const deleteBtnTop = document.getElementById("deleteSelectedBtnTop");
|
||||
|
||||
if (count > 0) {
|
||||
countEl.style.display = "inline-flex";
|
||||
countEl.querySelector(".count").textContent = count;
|
||||
deleteBtn.style.display = "inline-flex";
|
||||
deleteBtnTop.style.display = "inline-flex";
|
||||
} else {
|
||||
countEl.style.display = "none";
|
||||
deleteBtn.style.display = "none";
|
||||
deleteBtnTop.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,10 +25,10 @@
|
||||
/>
|
||||
|
||||
<!-- 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=20260118fix30"
|
||||
href="/assets/css/mobile-fixes.css?v=20260120fix2"
|
||||
/>
|
||||
|
||||
<style>
|
||||
@@ -608,7 +608,7 @@
|
||||
</div>
|
||||
|
||||
<div class="footer-bottom">
|
||||
<p>© 2026 Sky Art Shop. All rights reserved.</p>
|
||||
<p>© 2026 PromptTech-Solution. Designed and Developed by: PromptTech-Solution</p>
|
||||
<p>
|
||||
Made with
|
||||
<i class="bi bi-heart-fill" style="color: var(--primary-pink)"></i>
|
||||
|
||||
@@ -148,34 +148,96 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
/* All mobile devices - Slide-in menu from right (half screen) */
|
||||
@media (max-width: 768px) {
|
||||
/* Hide close button on desktop */
|
||||
.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 {
|
||||
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 {
|
||||
position: fixed !important;
|
||||
top: 0 !important;
|
||||
right: 0 !important;
|
||||
left: auto !important;
|
||||
bottom: 0 !important;
|
||||
width: 50% !important;
|
||||
min-width: 200px !important;
|
||||
max-width: 300px !important;
|
||||
width: 320px !important;
|
||||
max-width: 80% !important;
|
||||
height: 100vh !important;
|
||||
height: 100dvh !important; /* Dynamic viewport height for mobile browsers */
|
||||
flex-direction: column !important;
|
||||
background: #ffffff !important;
|
||||
padding: 80px 20px 30px !important;
|
||||
gap: 4px !important;
|
||||
padding: 100px 24px 30px !important;
|
||||
gap: 8px !important;
|
||||
transform: translateX(100%) !important;
|
||||
opacity: 1 !important;
|
||||
visibility: hidden !important;
|
||||
transition: transform 0.3s ease, visibility 0.3s ease !important;
|
||||
z-index: 999 !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;
|
||||
}
|
||||
|
||||
@@ -184,6 +246,30 @@ body {
|
||||
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 {
|
||||
list-style: none !important;
|
||||
margin: 0 !important;
|
||||
@@ -193,12 +279,12 @@ body {
|
||||
.nav-link {
|
||||
display: block !important;
|
||||
width: 100% !important;
|
||||
padding: 14px 16px !important;
|
||||
padding: 16px 20px !important;
|
||||
text-align: left !important;
|
||||
font-size: 1rem !important;
|
||||
font-size: 1.1rem !important;
|
||||
font-weight: 500 !important;
|
||||
color: #333 !important;
|
||||
border-radius: 8px !important;
|
||||
border-radius: 10px !important;
|
||||
text-decoration: none !important;
|
||||
transition: background 0.15s ease !important;
|
||||
}
|
||||
@@ -215,6 +301,82 @@ body {
|
||||
.nav-link::after {
|
||||
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) === */
|
||||
@@ -442,6 +604,13 @@ body {
|
||||
max-width: 100% !important;
|
||||
overflow: hidden !important;
|
||||
box-sizing: border-box !important;
|
||||
flex: 1 !important;
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
}
|
||||
|
||||
.product-footer {
|
||||
margin-top: auto !important;
|
||||
}
|
||||
|
||||
.product-name {
|
||||
@@ -485,6 +654,18 @@ body {
|
||||
overflow: hidden !important;
|
||||
box-sizing: border-box !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 {
|
||||
@@ -530,6 +711,18 @@ body {
|
||||
min-width: 0 !important;
|
||||
box-sizing: border-box !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 {
|
||||
@@ -2185,13 +2378,23 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
/* === GLOBAL PRODUCT CARD FIX - Remove pink divider === */
|
||||
/* === GLOBAL PRODUCT CARD FIX - Remove pink divider & Align footer === */
|
||||
.product-card {
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
background: #ffffff !important;
|
||||
}
|
||||
|
||||
.product-info {
|
||||
flex: 1 !important;
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
}
|
||||
|
||||
.product-footer {
|
||||
margin-top: auto !important;
|
||||
}
|
||||
|
||||
.product-image {
|
||||
background: #ffffff !important;
|
||||
margin: 0 !important;
|
||||
@@ -2204,3 +2407,205 @@ body {
|
||||
margin: 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;
|
||||
}
|
||||
}
|
||||
@@ -370,10 +370,10 @@ body {
|
||||
transition: var(--transition-fast);
|
||||
}
|
||||
|
||||
/* Mobile Navigation - Slide from Right (Half Screen) */
|
||||
@media (max-width: 992px) {
|
||||
/* Mobile Navigation - Slide from Right (all devices up to 1024px including iPads) */
|
||||
@media (max-width: 1024px) {
|
||||
.nav-mobile-toggle {
|
||||
display: flex;
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
.nav-menu {
|
||||
@@ -381,14 +381,13 @@ body {
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 50%;
|
||||
min-width: 200px;
|
||||
max-width: 300px;
|
||||
width: 320px;
|
||||
max-width: 80%;
|
||||
flex-direction: column;
|
||||
background: var(--bg-white);
|
||||
padding: 80px var(--spacing-lg) var(--spacing-xl);
|
||||
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%);
|
||||
visibility: hidden;
|
||||
transition: transform 0.3s ease, visibility 0.3s ease;
|
||||
@@ -402,9 +401,10 @@ body {
|
||||
|
||||
.nav-link {
|
||||
width: 100%;
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
padding: var(--spacing-md) var(--spacing-lg);
|
||||
text-align: left;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.nav-link:hover {
|
||||
@@ -828,6 +828,9 @@ body {
|
||||
|
||||
.product-info {
|
||||
padding: var(--spacing-lg);
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.product-category {
|
||||
@@ -883,6 +886,7 @@ body {
|
||||
justify-content: space-between;
|
||||
padding: var(--spacing-md) var(--spacing-lg);
|
||||
border-top: 1px solid var(--border-light);
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.product-stock {
|
||||
|
||||
@@ -405,14 +405,14 @@
|
||||
background: #f3f0ff;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
/* Responsive Design - Mobile & Tablet (up to 1024px includes all iPads) */
|
||||
@media (max-width: 1024px) {
|
||||
.navbar-menu {
|
||||
display: none;
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.mobile-toggle {
|
||||
display: flex;
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
@@ -423,6 +423,17 @@
|
||||
.navbar-actions {
|
||||
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) {
|
||||
@@ -456,13 +467,88 @@
|
||||
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
|
||||
* 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 */
|
||||
@media (max-width: 768px) {
|
||||
/* Mobile/Tablet hamburger menu - visible on screens up to 1024px (includes iPads) */
|
||||
@media (max-width: 1024px) {
|
||||
.mobile-toggle {
|
||||
display: flex !important;
|
||||
flex-direction: column;
|
||||
@@ -676,8 +762,17 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Tablet adjustments */
|
||||
@media (min-width: 769px) and (max-width: 1024px) {
|
||||
/* Desktop - hide mobile elements (only on screens larger than 1024px) */
|
||||
@media (min-width: 1025px) {
|
||||
.mobile-toggle {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.mobile-menu,
|
||||
.mobile-menu-overlay {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.navbar-actions {
|
||||
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 */
|
||||
.action-btn:focus,
|
||||
.mobile-toggle:focus,
|
||||
|
||||
BIN
website/public/assets/images/prompttech-copyright.png
Normal file
BIN
website/public/assets/images/prompttech-copyright.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
@@ -41,19 +41,67 @@ const SkyArtShop = {
|
||||
});
|
||||
}
|
||||
|
||||
// Mobile menu toggle
|
||||
// Mobile/Tablet menu toggle
|
||||
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) => {
|
||||
e.stopPropagation();
|
||||
navMenu.classList.toggle("open");
|
||||
mobileToggle.classList.toggle("active");
|
||||
if (navMenu.classList.contains("open")) {
|
||||
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
|
||||
navMenu.querySelectorAll(".nav-link").forEach((link) => {
|
||||
link.addEventListener("click", () => {
|
||||
navMenu.classList.remove("open");
|
||||
mobileToggle.classList.remove("active");
|
||||
closeMenu();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -64,12 +112,11 @@ const SkyArtShop = {
|
||||
!navMenu.contains(e.target) &&
|
||||
!mobileToggle.contains(e.target)
|
||||
) {
|
||||
navMenu.classList.remove("open");
|
||||
mobileToggle.classList.remove("active");
|
||||
closeMenu();
|
||||
}
|
||||
});
|
||||
|
||||
// Close menu when touching outside (for mobile)
|
||||
// Close menu when touching outside (for mobile/tablet)
|
||||
document.addEventListener(
|
||||
"touchstart",
|
||||
(e) => {
|
||||
@@ -78,12 +125,18 @@ const SkyArtShop = {
|
||||
!navMenu.contains(e.target) &&
|
||||
!mobileToggle.contains(e.target)
|
||||
) {
|
||||
navMenu.classList.remove("open");
|
||||
mobileToggle.classList.remove("active");
|
||||
closeMenu();
|
||||
}
|
||||
},
|
||||
{ passive: true },
|
||||
);
|
||||
|
||||
// Close menu on escape key
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Escape" && navMenu.classList.contains("open")) {
|
||||
closeMenu();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Set active nav link
|
||||
|
||||
@@ -25,13 +25,10 @@
|
||||
/>
|
||||
|
||||
<!-- Modern Theme CSS -->
|
||||
<link rel="stylesheet" href="/assets/css/modern-theme.css?v=fix35" />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="/assets/css/modern-theme.css?v=fix33"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="/assets/css/mobile-fixes.css?v=20260118fix10"
|
||||
href="/assets/css/mobile-fixes.css?v=20260120fix2"
|
||||
/>
|
||||
|
||||
<style>
|
||||
@@ -885,11 +882,14 @@
|
||||
</p>
|
||||
<form
|
||||
class="newsletter-form"
|
||||
id="blogNewsletterForm"
|
||||
style="max-width: 500px; margin: 0 auto; display: flex; gap: 12px"
|
||||
>
|
||||
<input
|
||||
type="email"
|
||||
id="blogNewsletterEmail"
|
||||
placeholder="Enter your email"
|
||||
required
|
||||
style="
|
||||
flex: 1;
|
||||
padding: 16px 24px;
|
||||
@@ -898,7 +898,13 @@
|
||||
font-size: 1rem;
|
||||
"
|
||||
/>
|
||||
<button type="submit" class="btn btn-primary">Subscribe</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary"
|
||||
id="blogNewsletterBtn"
|
||||
>
|
||||
Subscribe
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
@@ -921,15 +927,63 @@
|
||||
collaging stationery. Quality products for all your creative
|
||||
needs.
|
||||
</p>
|
||||
<div class="footer-social" id="footerSocialLinks">
|
||||
<a href="#" class="social-link" id="footerFacebook" style="display:none;"><i class="bi bi-facebook"></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 class="footer-social" id="footerSocialLinks">
|
||||
<a
|
||||
href="#"
|
||||
class="social-link"
|
||||
id="footerFacebook"
|
||||
style="display: none"
|
||||
><i class="bi bi-facebook"></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>
|
||||
|
||||
@@ -966,7 +1020,7 @@
|
||||
</div>
|
||||
|
||||
<div class="footer-bottom">
|
||||
<p>© 2026 Sky Art Shop. All rights reserved.</p>
|
||||
<p>© 2026 PromptTech-Solution. Designed and Developed by: PromptTech-Solution</p>
|
||||
<p>
|
||||
Made with
|
||||
<i class="bi bi-heart-fill" style="color: var(--primary-pink)"></i>
|
||||
@@ -1373,6 +1427,61 @@
|
||||
document.addEventListener("keydown", (e) => {
|
||||
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 src="/assets/js/accessibility.js"></script>
|
||||
|
||||
@@ -22,8 +22,8 @@
|
||||
/>
|
||||
|
||||
<!-- Modern Theme CSS -->
|
||||
<link rel="stylesheet" href="/assets/css/modern-theme.css?v=fix33" />
|
||||
<link rel="stylesheet" href="/assets/css/mobile-fixes.css?v=20260118c" />
|
||||
<link rel="stylesheet" href="/assets/css/modern-theme.css?v=fix35" />
|
||||
<link rel="stylesheet" href="/assets/css/mobile-fixes.css?v=20260120fix2" />
|
||||
|
||||
<style>
|
||||
.checkout-container {
|
||||
@@ -640,7 +640,7 @@
|
||||
</div>
|
||||
|
||||
<div class="footer-bottom">
|
||||
<p>© 2026 Sky Art Shop. All rights reserved.</p>
|
||||
<p>© 2026 PromptTech-Solution. Designed and Developed by: PromptTech-Solution</p>
|
||||
<p>
|
||||
Made with
|
||||
<i class="bi bi-heart-fill" style="color: var(--primary-pink)"></i>
|
||||
|
||||
@@ -25,10 +25,10 @@
|
||||
/>
|
||||
|
||||
<!-- 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=20260118fix10"
|
||||
href="/assets/css/mobile-fixes.css?v=20260120fix2"
|
||||
/>
|
||||
|
||||
<style>
|
||||
@@ -671,7 +671,7 @@
|
||||
</div>
|
||||
|
||||
<div class="footer-bottom">
|
||||
<p>© 2026 Sky Art Shop. All rights reserved.</p>
|
||||
<p>© 2026 PromptTech-Solution. Designed and Developed by: PromptTech-Solution</p>
|
||||
<p>
|
||||
Made with
|
||||
<i class="bi bi-heart-fill" style="color: var(--primary-pink)"></i>
|
||||
@@ -835,16 +835,62 @@
|
||||
|
||||
document
|
||||
.getElementById("contactForm")
|
||||
.addEventListener("submit", function (e) {
|
||||
.addEventListener("submit", async function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
// Show success notification
|
||||
SkyArtShop.showNotification(
|
||||
"Message sent successfully! We'll get back to you soon.",
|
||||
);
|
||||
const form = this;
|
||||
const submitBtn = form.querySelector('button[type="submit"]');
|
||||
const originalBtnText = submitBtn.innerHTML;
|
||||
|
||||
// Reset form
|
||||
this.reset();
|
||||
// Get form data
|
||||
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 src="/assets/js/accessibility.js"></script>
|
||||
|
||||
@@ -25,8 +25,8 @@
|
||||
/>
|
||||
|
||||
<!-- Modern Theme CSS -->
|
||||
<link rel="stylesheet" href="/assets/css/modern-theme.css?v=fix33" />
|
||||
<link rel="stylesheet" href="/assets/css/mobile-fixes.css?v=20260118c" />
|
||||
<link rel="stylesheet" href="/assets/css/modern-theme.css?v=fix35" />
|
||||
<link rel="stylesheet" href="/assets/css/mobile-fixes.css?v=20260120fix2" />
|
||||
|
||||
<style>
|
||||
.faq-container {
|
||||
@@ -252,7 +252,7 @@
|
||||
</div>
|
||||
|
||||
<div class="footer-bottom">
|
||||
<p>© 2026 Sky Art Shop. All rights reserved.</p>
|
||||
<p>© 2026 PromptTech-Solution. Designed and Developed by: PromptTech-Solution</p>
|
||||
<p>
|
||||
Made with
|
||||
<i class="bi bi-heart-fill" style="color: var(--primary-pink)"></i>
|
||||
|
||||
@@ -25,20 +25,59 @@
|
||||
/>
|
||||
|
||||
<!-- Modern Theme CSS -->
|
||||
<link rel="stylesheet" href="/assets/css/modern-theme.css?v=fix33" />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="/assets/css/mobile-fixes.css?v=20260119touch"
|
||||
/>
|
||||
<link rel="stylesheet" href="/assets/css/modern-theme.css?v=fix35" />
|
||||
<link rel="stylesheet" href="/assets/css/mobile-fixes.css?v=20260120fix2" />
|
||||
|
||||
<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 {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: var(--spacing-lg);
|
||||
max-width: 1100px;
|
||||
margin: 0 auto;
|
||||
display: flex !important;
|
||||
flex-wrap: nowrap !important;
|
||||
overflow-x: auto !important;
|
||||
overflow-y: hidden !important;
|
||||
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 */
|
||||
@@ -157,15 +196,21 @@
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Tablet - Blog cards horizontal scroll */
|
||||
@media (max-width: 992px) {
|
||||
#inspirationGrid.blog-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
#inspirationGrid.blog-grid .blog-card {
|
||||
flex: 0 0 280px;
|
||||
min-width: 280px;
|
||||
max-width: 280px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile - Blog cards horizontal scroll */
|
||||
@media (max-width: 576px) {
|
||||
#inspirationGrid.blog-grid {
|
||||
grid-template-columns: 1fr;
|
||||
#inspirationGrid.blog-grid .blog-card {
|
||||
flex: 0 0 240px;
|
||||
min-width: 240px;
|
||||
max-width: 240px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -307,7 +352,7 @@
|
||||
<section
|
||||
class="section"
|
||||
id="featured-products"
|
||||
style="background: var(--primary-pink-light)"
|
||||
style="background: var(--primary-pink-light); padding: 30px 0"
|
||||
>
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
@@ -336,7 +381,7 @@
|
||||
<section
|
||||
class="section inspiration-section"
|
||||
id="get-inspired"
|
||||
style="background: var(--accent-pink)"
|
||||
style="background: var(--accent-pink); padding: 30px 0"
|
||||
>
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
@@ -362,10 +407,10 @@
|
||||
<section
|
||||
class="section"
|
||||
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="about-content">
|
||||
<div class="about-content" style="padding: 10px 0">
|
||||
<div class="about-text">
|
||||
<h2 id="aboutTitle">About Sky Art Shop</h2>
|
||||
<div id="aboutDescription">
|
||||
@@ -407,13 +452,8 @@
|
||||
<!-- Newsletter -->
|
||||
<section
|
||||
class="section"
|
||||
style="
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--primary-pink-light) 0%,
|
||||
var(--primary-pink) 100%
|
||||
);
|
||||
"
|
||||
id="newsletter-section"
|
||||
style="background: var(--accent-pink); padding: 40px 0"
|
||||
>
|
||||
<div class="container">
|
||||
<div class="text-center">
|
||||
@@ -424,11 +464,14 @@
|
||||
</p>
|
||||
<form
|
||||
class="newsletter-form"
|
||||
id="homeNewsletterForm"
|
||||
style="max-width: 500px; margin: 0 auto; display: flex; gap: 12px"
|
||||
>
|
||||
<input
|
||||
type="email"
|
||||
id="homeNewsletterEmail"
|
||||
placeholder="Enter your email"
|
||||
required
|
||||
style="
|
||||
flex: 1;
|
||||
padding: 16px 24px;
|
||||
@@ -437,7 +480,13 @@
|
||||
font-size: 1rem;
|
||||
"
|
||||
/>
|
||||
<button type="submit" class="btn btn-primary">Subscribe</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary"
|
||||
id="homeNewsletterBtn"
|
||||
>
|
||||
Subscribe
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -519,6 +568,10 @@
|
||||
><i class="bi bi-linkedin"></i
|
||||
></a>
|
||||
</div>
|
||||
<p style="margin-top: 12px; font-size: 0.85rem; opacity: 0.9">
|
||||
© 2026 PromptTech-Solution.<br />Designed and Developed by:
|
||||
PromptTech-Solution
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="footer-column">
|
||||
@@ -554,7 +607,7 @@
|
||||
</div>
|
||||
|
||||
<div class="footer-bottom">
|
||||
<p>© 2026 Sky Art Shop. All rights reserved.</p>
|
||||
<p>© 2026 PromptTech-Solution. Designed and Developed by: PromptTech-Solution</p>
|
||||
<p>
|
||||
Made with
|
||||
<i class="bi bi-heart-fill" style="color: var(--primary-pink)"></i>
|
||||
@@ -1010,6 +1063,61 @@
|
||||
heroSlider.innerHTML =
|
||||
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 src="/assets/js/accessibility.js"></script>
|
||||
</body>
|
||||
|
||||
@@ -27,11 +27,11 @@
|
||||
<!-- Modern Theme CSS -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="/assets/css/modern-theme.css?v=fix33"
|
||||
href="/assets/css/modern-theme.css?v=fix35"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="/assets/css/mobile-fixes.css?v=20260118fix10"
|
||||
href="/assets/css/mobile-fixes.css?v=20260120fix2"
|
||||
/>
|
||||
|
||||
<style>
|
||||
@@ -567,7 +567,7 @@
|
||||
</div>
|
||||
|
||||
<div class="footer-bottom">
|
||||
<p>© 2026 Sky Art Shop. All rights reserved.</p>
|
||||
<p>© 2026 PromptTech-Solution. Designed and Developed by: PromptTech-Solution</p>
|
||||
<p>
|
||||
Made with
|
||||
<i class="bi bi-heart-fill" style="color: var(--primary-pink)"></i>
|
||||
|
||||
@@ -22,8 +22,8 @@
|
||||
/>
|
||||
|
||||
<!-- Modern Theme CSS -->
|
||||
<link rel="stylesheet" href="/assets/css/modern-theme.css?v=fix33" />
|
||||
<link rel="stylesheet" href="/assets/css/mobile-fixes.css?v=fix32" />
|
||||
<link rel="stylesheet" href="/assets/css/modern-theme.css?v=fix35" />
|
||||
<link rel="stylesheet" href="/assets/css/mobile-fixes.css?v=20260120fix2" />
|
||||
|
||||
<style>
|
||||
.policy-container {
|
||||
@@ -278,7 +278,7 @@
|
||||
</div>
|
||||
|
||||
<div class="footer-bottom">
|
||||
<p>© 2026 Sky Art Shop. All rights reserved.</p>
|
||||
<p>© 2026 PromptTech-Solution. Designed and Developed by: PromptTech-Solution</p>
|
||||
<p>
|
||||
Made with
|
||||
<i class="bi bi-heart-fill" style="color: var(--primary-pink)"></i>
|
||||
|
||||
@@ -22,8 +22,8 @@
|
||||
/>
|
||||
|
||||
<!-- Modern Theme CSS -->
|
||||
<link rel="stylesheet" href="/assets/css/modern-theme.css?v=fix34" />
|
||||
<link rel="stylesheet" href="/assets/css/mobile-fixes.css?v=20260119fix3" />
|
||||
<link rel="stylesheet" href="/assets/css/modern-theme.css?v=fix35" />
|
||||
<link rel="stylesheet" href="/assets/css/mobile-fixes.css?v=20260120fix2" />
|
||||
|
||||
<style>
|
||||
/* 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 {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
@@ -134,8 +192,11 @@
|
||||
}
|
||||
|
||||
.main-image {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
aspect-ratio: 1;
|
||||
max-width: 100%;
|
||||
height: 0;
|
||||
padding-bottom: 100%; /* 1:1 aspect ratio */
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
background: var(--bg-white);
|
||||
@@ -143,12 +204,79 @@
|
||||
}
|
||||
|
||||
.main-image img {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
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 {
|
||||
display: flex;
|
||||
gap: var(--spacing-sm);
|
||||
@@ -679,11 +807,25 @@
|
||||
<!-- Gallery -->
|
||||
<div class="product-gallery">
|
||||
<div class="main-image">
|
||||
<button
|
||||
class="gallery-arrow prev"
|
||||
id="galleryPrev"
|
||||
aria-label="Previous image"
|
||||
>
|
||||
<i class="bi bi-chevron-left"></i>
|
||||
</button>
|
||||
<img
|
||||
src="https://images.unsplash.com/photo-1513519245088-0e12902e35a6?w=800&q=80"
|
||||
alt="Product"
|
||||
id="mainImage"
|
||||
/>
|
||||
<button
|
||||
class="gallery-arrow next"
|
||||
id="galleryNext"
|
||||
aria-label="Next image"
|
||||
>
|
||||
<i class="bi bi-chevron-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="thumbnail-gallery" id="thumbnailGallery">
|
||||
<!-- Thumbnails loaded via JS -->
|
||||
@@ -939,7 +1081,10 @@
|
||||
</div>
|
||||
|
||||
<div class="footer-bottom">
|
||||
<p>© 2026 Sky Art Shop. All rights reserved.</p>
|
||||
<p>
|
||||
© 2026 PromptTech-Solution. Designed and Developed by:
|
||||
PromptTech-Solution
|
||||
</p>
|
||||
<p>
|
||||
Made with
|
||||
<i class="bi bi-heart-fill" style="color: var(--primary-pink)"></i>
|
||||
@@ -1260,8 +1405,76 @@
|
||||
.forEach((t) => t.classList.remove("active"));
|
||||
thumb.classList.add("active");
|
||||
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
|
||||
|
||||
@@ -25,8 +25,8 @@
|
||||
/>
|
||||
|
||||
<!-- Modern Theme CSS -->
|
||||
<link rel="stylesheet" href="/assets/css/modern-theme.css?v=fix33" />
|
||||
<link rel="stylesheet" href="/assets/css/mobile-fixes.css?v=fix32" />
|
||||
<link rel="stylesheet" href="/assets/css/modern-theme.css?v=fix35" />
|
||||
<link rel="stylesheet" href="/assets/css/mobile-fixes.css?v=20260120fix2" />
|
||||
|
||||
<style>
|
||||
.policy-container {
|
||||
@@ -292,7 +292,7 @@
|
||||
</div>
|
||||
|
||||
<div class="footer-bottom">
|
||||
<p>© 2026 Sky Art Shop. All rights reserved.</p>
|
||||
<p>© 2026 PromptTech-Solution. Designed and Developed by: PromptTech-Solution</p>
|
||||
<p>
|
||||
Made with
|
||||
<i class="bi bi-heart-fill" style="color: var(--primary-pink)"></i>
|
||||
|
||||
@@ -22,8 +22,8 @@
|
||||
/>
|
||||
|
||||
<!-- Modern Theme CSS -->
|
||||
<link rel="stylesheet" href="/assets/css/modern-theme.css?v=fix33" />
|
||||
<link rel="stylesheet" href="/assets/css/mobile-fixes.css?v=fix32" />
|
||||
<link rel="stylesheet" href="/assets/css/modern-theme.css?v=fix35" />
|
||||
<link rel="stylesheet" href="/assets/css/mobile-fixes.css?v=20260120fix2" />
|
||||
|
||||
<style>
|
||||
.policy-container {
|
||||
@@ -331,7 +331,7 @@
|
||||
</div>
|
||||
|
||||
<div class="footer-bottom">
|
||||
<p>© 2026 Sky Art Shop. All rights reserved.</p>
|
||||
<p>© 2026 PromptTech-Solution. Designed and Developed by: PromptTech-Solution</p>
|
||||
<p>
|
||||
Made with
|
||||
<i class="bi bi-heart-fill" style="color: var(--primary-pink)"></i>
|
||||
|
||||
@@ -22,8 +22,8 @@
|
||||
/>
|
||||
|
||||
<!-- Modern Theme CSS -->
|
||||
<link rel="stylesheet" href="/assets/css/modern-theme.css?v=fix34" />
|
||||
<link rel="stylesheet" href="/assets/css/mobile-fixes.css?v=20260119fix1" />
|
||||
<link rel="stylesheet" href="/assets/css/modern-theme.css?v=fix35" />
|
||||
<link rel="stylesheet" href="/assets/css/mobile-fixes.css?v=20260120fix2" />
|
||||
<style>
|
||||
/* Mobile & Tablet: Shop page layout fixes */
|
||||
@media (max-width: 992px) {
|
||||
@@ -541,7 +541,10 @@
|
||||
</div>
|
||||
|
||||
<div class="footer-bottom">
|
||||
<p>© 2026 Sky Art Shop. All rights reserved.</p>
|
||||
<p>
|
||||
© 2026 PromptTech-Solution. Designed and Developed by:
|
||||
PromptTech-Solution
|
||||
</p>
|
||||
<p>
|
||||
Made with
|
||||
<i class="bi bi-heart-fill" style="color: var(--primary-pink)"></i>
|
||||
|
||||
Reference in New Issue
Block a user