webupdate
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
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 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)
|
||||||
|
|||||||
@@ -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";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>© 2026 Sky Art Shop. All rights reserved.</p>
|
<p>© 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>
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -2203,4 +2406,206 @@ body {
|
|||||||
display: block !important;
|
display: block !important;
|
||||||
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
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) {
|
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
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -921,15 +927,63 @@
|
|||||||
collaging stationery. Quality products for all your creative
|
collaging stationery. Quality products for all your creative
|
||||||
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>© 2026 Sky Art Shop. All rights reserved.</p>
|
<p>© 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>
|
||||||
|
|||||||
@@ -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>© 2026 Sky Art Shop. All rights reserved.</p>
|
<p>© 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>
|
||||||
|
|||||||
@@ -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>© 2026 Sky Art Shop. All rights reserved.</p>
|
<p>© 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>
|
||||||
|
|||||||
@@ -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>© 2026 Sky Art Shop. All rights reserved.</p>
|
<p>© 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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
© 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>© 2026 Sky Art Shop. All rights reserved.</p>
|
<p>© 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>
|
||||||
|
|||||||
@@ -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>© 2026 Sky Art Shop. All rights reserved.</p>
|
<p>© 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>
|
||||||
|
|||||||
@@ -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>© 2026 Sky Art Shop. All rights reserved.</p>
|
<p>© 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>
|
||||||
|
|||||||
@@ -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>© 2026 Sky Art Shop. All rights reserved.</p>
|
<p>
|
||||||
|
© 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
|
||||||
|
|||||||
@@ -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>© 2026 Sky Art Shop. All rights reserved.</p>
|
<p>© 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>
|
||||||
|
|||||||
@@ -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>© 2026 Sky Art Shop. All rights reserved.</p>
|
<p>© 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>
|
||||||
|
|||||||
@@ -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>© 2026 Sky Art Shop. All rights reserved.</p>
|
<p>
|
||||||
|
© 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>
|
||||||
|
|||||||
Reference in New Issue
Block a user