From 1b2502c38d0b19d9ffe09721c98536cd9b8f55d3 Mon Sep 17 00:00:00 2001 From: Local Server Date: Tue, 20 Jan 2026 20:29:33 -0600 Subject: [PATCH] webupdate --- backend/.env | 4 +- .../009_create_contact_newsletter.sql | 36 ++ backend/routes/contact-newsletter.js | 394 ++++++++++++++++ backend/server.js | 2 + website/admin/media-library.html | 353 ++++++++++++-- website/public/about.html | 6 +- website/public/assets/css/mobile-fixes.css | 429 +++++++++++++++++- website/public/assets/css/modern-theme.css | 20 +- website/public/assets/css/navbar.css | 123 ++++- .../assets/images/prompttech-copyright.png | Bin 0 -> 26074 bytes website/public/assets/js/modern-theme.js | 73 ++- website/public/blog.html | 141 +++++- website/public/checkout.html | 6 +- website/public/contact.html | 66 ++- website/public/faq.html | 6 +- website/public/home.html | 164 +++++-- website/public/portfolio.html | 6 +- website/public/privacy.html | 6 +- website/public/product.html | 221 ++++++++- website/public/returns.html | 6 +- website/public/shipping-info.html | 6 +- website/public/shop.html | 9 +- 22 files changed, 1905 insertions(+), 172 deletions(-) create mode 100644 backend/migrations/009_create_contact_newsletter.sql create mode 100644 backend/routes/contact-newsletter.js create mode 100644 website/public/assets/images/prompttech-copyright.png diff --git a/backend/.env b/backend/.env index a9eeccf..df2f55f 100644 --- a/backend/.env +++ b/backend/.env @@ -28,6 +28,6 @@ MAX_FILE_SIZE=62914560 SMTP_HOST=smtp.gmail.com SMTP_PORT=587 SMTP_SECURE=false -SMTP_USER=YOUR_GMAIL@gmail.com +SMTP_USER=skyartshop12.11@gmail.com SMTP_PASS=YOUR_APP_PASSWORD -SMTP_FROM="Sky Art Shop" +SMTP_FROM="Sky Art Shop" diff --git a/backend/migrations/009_create_contact_newsletter.sql b/backend/migrations/009_create_contact_newsletter.sql new file mode 100644 index 0000000..7398633 --- /dev/null +++ b/backend/migrations/009_create_contact_newsletter.sql @@ -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'; diff --git a/backend/routes/contact-newsletter.js b/backend/routes/contact-newsletter.js new file mode 100644 index 0000000..5786d54 --- /dev/null +++ b/backend/routes/contact-newsletter.js @@ -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: ` +
+
+

📬 New Contact Message

+
+ +
+

+ From: ${sanitizedName} +

+

+ Email: ${sanitizedEmail} +

+

+ Subject: ${sanitizedSubject} +

+
+

+ Message: +

+
+ ${sanitizedMessage.replace(/\n/g, "
")} +
+
+ +

+ © ${new Date().getFullYear()} Sky Art Shop - Contact Form Notification +

+
+ `, + }); + + // 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: ` +
+
+

🎨 Thank You!

+
+ +
+

+ Hi ${sanitizedName}, +

+

+ Thank you for reaching out to Sky Art Shop! We've received your message and will get back to you within 24-48 hours. +

+

+ Here's a copy of your message: +

+
+

Subject: ${sanitizedSubject}

+

${sanitizedMessage.replace(/\n/g, "
")}

+
+

+ In the meantime, feel free to explore our collection at our shop! +

+

+ Best regards,
+ The Sky Art Shop Team +

+
+ +

+ © ${new Date().getFullYear()} Sky Art Shop +

+
+ `, + }); + + 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: ` +
+
+

🎨 Welcome to Sky Art Shop!

+
+ +
+

+ Thank you for subscribing to our newsletter! You're now part of our creative community. +

+

+ Here's what you can expect: +

+
    +
  • 🛍️ Exclusive deals and discounts
  • +
  • ✨ New product announcements
  • +
  • 💡 Creative tips and inspiration
  • +
  • 🎁 Special subscriber-only offers
  • +
+ +
+ +

+ © ${new Date().getFullYear()} Sky Art Shop
+ Unsubscribe +

+
+ `, + }); + 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; diff --git a/backend/server.js b/backend/server.js index f0cb4bc..f42422e 100644 --- a/backend/server.js +++ b/backend/server.js @@ -257,6 +257,7 @@ const usersRoutes = require("./routes/users"); const uploadRoutes = require("./routes/upload"); const customerAuthRoutes = require("./routes/customer-auth"); const customerCartRoutes = require("./routes/customer-cart"); +const contactNewsletterRoutes = require("./routes/contact-newsletter"); // Admin redirect - handle /admin to redirect to login (must be before static files) app.get("/admin", (req, res) => { @@ -330,6 +331,7 @@ app.use("/api/admin/users", usersRoutes); app.use("/api/admin", uploadRoutes); app.use("/api/customers", customerAuthRoutes); app.use("/api/customers", customerCartRoutes); +app.use("/api", contactNewsletterRoutes); app.use("/api", publicRoutes); // Admin static files (must be after URL rewriting) diff --git a/website/admin/media-library.html b/website/admin/media-library.html index 4c14720..583b3f0 100644 --- a/website/admin/media-library.html +++ b/website/admin/media-library.html @@ -69,6 +69,126 @@ .stat-item i { font-size: 1rem; } + + /* Skeleton Loading Animation */ + .skeleton-item { + background: #f8fafc; + border-radius: 12px; + overflow: hidden; + pointer-events: none; + } + + .skeleton-preview { + width: 100%; + aspect-ratio: 1; + background: linear-gradient( + 90deg, + #f0f0f0 25%, + #e0e0e0 50%, + #f0f0f0 75% + ); + background-size: 200% 100%; + animation: skeleton-shimmer 1.5s infinite; + } + + .skeleton-info { + padding: 12px; + } + + .skeleton-name { + height: 14px; + background: linear-gradient( + 90deg, + #f0f0f0 25%, + #e0e0e0 50%, + #f0f0f0 75% + ); + background-size: 200% 100%; + animation: skeleton-shimmer 1.5s infinite; + border-radius: 4px; + margin-bottom: 8px; + width: 80%; + } + + .skeleton-meta { + height: 12px; + background: linear-gradient( + 90deg, + #f0f0f0 25%, + #e0e0e0 50%, + #f0f0f0 75% + ); + background-size: 200% 100%; + animation: skeleton-shimmer 1.5s infinite; + border-radius: 4px; + width: 50%; + } + + @keyframes skeleton-shimmer { + 0% { + background-position: 200% 0; + } + 100% { + background-position: -200% 0; + } + } + + /* Lazy loading image placeholder - instant load, no delay */ + .media-item .item-preview img { + opacity: 1; + } + + .media-item .item-preview img.loaded { + opacity: 1; + } + + .media-item .item-preview { + background: #f0f0f0; + display: flex; + align-items: center; + justify-content: center; + } + + /* Loading indicator at bottom */ + .load-more-indicator { + text-align: center; + padding: 20px; + color: #666; + display: none; + } + + .load-more-indicator.visible { + display: block; + } + + /* Fixed toolbar styles when scrolling */ + .media-library-page { + position: relative; + } + + .media-library-toolbar { + background: white; + transition: box-shadow 0.2s ease; + } + + .media-library-toolbar.fixed-toolbar { + position: fixed; + top: 0; + z-index: 1000; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); + border-radius: 0 !important; + border-top-left-radius: 0 !important; + border-top-right-radius: 0 !important; + } + + /* Placeholder to prevent content jump when toolbar becomes fixed */ + .toolbar-placeholder { + display: none; + } + + .toolbar-placeholder.visible { + display: block; + } @@ -162,6 +282,14 @@ > New Folder +